Apply form hook for room page

This commit is contained in:
2023-11-04 13:23:20 +07:00
parent 32a65038f5
commit 425610d654
11 changed files with 208 additions and 473 deletions
@@ -1,5 +1,11 @@
import { FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'; import {
FieldValues,
RegisterOptions,
UseFormRegister,
useFormContext,
} from 'react-hook-form';
import { StyledSelect } from './styled'; import { StyledSelect } from './styled';
import React, { ReactNode } from 'react';
export interface ISelectOptions { export interface ISelectOptions {
value: string; value: string;
@@ -8,31 +14,81 @@ export interface ISelectOptions {
interface ISelect { interface ISelect {
options: ISelectOptions[]; options: ISelectOptions[];
optionsConfigForm?: RegisterOptions<FieldValues, string> | undefined; optionsConfigForm?: RegisterOptions<FieldValues, string> | undefined;
value?: number; value?: string;
id: string; onChange?: React.ChangeEventHandler<HTMLSelectElement> | undefined;
id?: string;
ariaLabel: string;
} }
const Select = ({ options, id, optionsConfigForm }: ISelect) => { interface IRender extends ISelect {
const { register } = useFormContext() ?? {}; register: UseFormRegister<FieldValues>;
children: ReactNode[];
}
if(!register) { const RenderSelect = ({
return null; options,
id,
value,
optionsConfigForm,
onChange,
register,
children,
ariaLabel,
}: IRender) => {
if (options && !register) {
return (
<StyledSelect
aria-label={ariaLabel}
id={id}
value={value}
onChange={onChange}
>
{children}
</StyledSelect>
);
} }
return (
<StyledSelect
aria-label={ariaLabel}
id={id}
value={value}
{...register(id!, optionsConfigForm)}
onChange={onChange}
>
{children}
</StyledSelect>
);
};
const Select = ({
options,
id,
optionsConfigForm,
value,
onChange,
ariaLabel
}: ISelect) => {
const { register } = useFormContext() ?? {};
if (!options) return; if (!options) return;
return ( return (
<StyledSelect <RenderSelect
aria-label="Sort" register={register}
options={options}
optionsConfigForm={optionsConfigForm}
id={id} id={id}
{...register(id, optionsConfigForm)} value={value}
onChange={onChange}
ariaLabel={ariaLabel}
> >
{options.map((option) => ( {options.map((option) => (
<option value={option.value} key={option.value}> <option value={option.value} key={option.value}>
{option.label} {option.label}
</option> </option>
))} ))}
</StyledSelect> </RenderSelect>
); );
}; };
@@ -20,12 +20,12 @@ const SortBy = memo(({ options }: ISortByProps) => {
setSearchParams(searchParams); setSearchParams(searchParams);
}; };
// prettier-ignore
return ( return (
<Select <Select
options={options} options={options}
value={sortBy} value={sortBy}
onChange={handleChange} onChange={handleChange}
ariaLabel='Sort'
/> />
); );
}); });
+1 -1
View File
@@ -2,6 +2,6 @@ export const DASHBOARD = '/dashboard';
export const USER = '/user'; export const USER = '/user';
export const ROOM = '/room'; export const ROOM = '/room';
export const OTHER_PATH = '*'; export const OTHER_PATH = '*';
export const BASE_URL = 'http://localhost:3000/'; export const BASE_URL = 'https://hotel-management-api.loiphan.com/';
export const USER_PATH = 'users'; export const USER_PATH = 'users';
export const ROOM_PATH = 'rooms'; export const ROOM_PATH = 'rooms';
-1
View File
@@ -12,7 +12,6 @@ type TRoom = {
name: string; name: string;
price: number; price: number;
discount: number; discount: number;
description: string;
status: boolean; status: boolean;
}; };
+2 -15
View File
@@ -18,15 +18,6 @@ const isBool = (value: unknown) => {
return typeof value === 'boolean'; return typeof value === 'boolean';
}; };
/**
* The function check value has type object or not
* @param value The value need to checked
* @returns A boolean indicating whether or not the argument has type object.
*/
const isObject = (value: unknown) => {
return typeof value === 'object' && value !== null;
};
/** /**
* Set required error for value * Set required error for value
* @param value The value set required or not * @param value The value set required or not
@@ -94,19 +85,16 @@ const searchQuery = (
columnSearch: string, columnSearch: string,
keySearch: string, keySearch: string,
sort: string, sort: string,
order: string, order: string
) => { ) => {
// prettier-ignore
const phoneParams = keySearch const phoneParams = keySearch
? `${columnSearch}_like=` + keySearch ? `${columnSearch}_like=` + keySearch
: ''; : '';
// prettier-ignore
const sortParams = sort const sortParams = sort
? '_sort=' + sort ? '_sort=' + sort
: ''; : '';
// prettier-ignore
const orderParams = order const orderParams = order
? '_order=' + order ? '_order=' + order
: ''; : '';
@@ -129,10 +117,9 @@ const searchQuery = (
}; };
export { export {
isObject,
isRequired, isRequired,
getPropValues, getPropValues,
getValueFromObj, getValueFromObj,
searchQuery, searchQuery,
REQUIRED_FIELD_ERROR, REQUIRED_FIELD_ERROR
}; };
+2 -2
View File
@@ -3,8 +3,8 @@
* @param value Value need to be checked * @param value Value need to be checked
* @returns A boolean indicating whether or not the argument has valid * @returns A boolean indicating whether or not the argument has valid
*/ */
const isValidNumber = (value: string): boolean => { const isValidNumber = (value: unknown): boolean => {
return /^[0-9]*$/.test(value); return /^[0-9]*$/.test(value as string);
}; };
/** /**
+3 -2
View File
@@ -54,10 +54,11 @@ const useFetch = (
const response = await fetch(BASE_URL + path + '?' + query); const response = await fetch(BASE_URL + path + '?' + query);
const json = await response.json(); const json = await response.json();
if (!response.ok) if (!response.ok) {
throw new Error( throw new Error(
`Error code: ${response.status} \n Messages: ${response.statusText}`, `Error code: ${response.status} \n Messages: ${response.text}`,
); );
}
setIsPending(false); setIsPending(false);
setData(json); setData(json);
-170
View File
@@ -1,170 +0,0 @@
import { useState, useEffect, useCallback, ChangeEvent } from 'react';
// Utils
import { getPropValues, isObject, isRequired } from '../helpers/utils';
// Types
import { TKeyValue, TValidator } from '../globals/types';
// Constants
import { ERROR, INITIAL_STATE_SCHEMA, VALUE } from '../constants/variables';
/**
* Custom hooks to validate your Form...
*
* @param stateSchema model you stateSchema.
* @param stateValidatorSchema model your validation.
* @param submitFormCallback function to be execute during form submission.
* @returns
*/
const useForm = (
stateSchema = {},
stateValidatorSchema = {} as TValidator,
submitFormCallback: (values: TKeyValue) => void,
initialValue: string = '',
) => {
const [values, setValues] = useState<TKeyValue>(INITIAL_STATE_SCHEMA);
const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR));
const [valid, isValid] = useState(getPropValues(stateSchema));
const [disable, setDisable] = useState(true);
const [isDirty, setIsDirty] = useState(false);
// Get a local copy of stateSchema
useEffect(() => {
setInitialErrorState(initialValue);
setDisable(true);
isValid(getPropValues(stateSchema));
setValues(getPropValues(stateSchema, VALUE));
// If initial value true, setValues again from stateSchema
// and enabled button
if (initialValue) {
setValues(getPropValues(stateSchema, VALUE));
setDisable(false);
}
}, [initialValue]); // eslint-disable-line
// Validate fields in forms
const validateFormFields = useCallback(
(name: string, value: string) => {
const validator = stateValidatorSchema;
// Making sure that stateValidatorSchema name is same in
// stateSchema
if (!validator[name]) return;
const field = validator[name];
let error = '';
// Skip check id field
if (name !== 'id' && name !== 'roomId') {
error = isRequired(value, field!.required);
if (isObject(field['validator']) && error === '') {
const fieldValidator = field['validator'];
// Test the function callback if the value is meet the criteria
const testFunc = fieldValidator!['func'];
if (!testFunc!(value)) {
error = fieldValidator!['error']!;
}
}
}
return error;
},
[stateValidatorSchema],
);
// Set Initial Error State
// When hooks was first rendered...
const setInitialErrorState = useCallback(
(initialValue: string) => {
Object.keys(errors).map((name) =>
setErrors((prevState) => ({
...prevState,
[name]: !initialValue // Skip error when initialValue have values
? validateFormFields(name, values[name] as string)
: '',
})),
);
},
[errors, values, validateFormFields],
);
// Used to disable submit button if there's a value in errors
// or the required field in state has no value.
// Wrapped in useCallback to cached the function to avoid intensive memory leaked
// in every re-render in component
const validateErrorState = useCallback(
() => Object.values(errors).some((error) => error),
[errors],
);
// For every changed in our state this will be fired
// To be able to disable the button
useEffect(() => {
if (isDirty) {
setDisable(validateErrorState());
}
}, [errors, isDirty, validateErrorState]);
// Event handler for handling changes in input.
const handleOnChange = useCallback(
(
event: ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => {
setIsDirty(true);
let error = '';
const name = (event.target! as HTMLInputElement).name;
let value: string | boolean = '';
if ((event.target! as HTMLInputElement).type === 'checkbox') {
value = (event.target! as HTMLInputElement).checked;
} else {
value = (event.target! as HTMLInputElement).value;
}
if (typeof value === 'string') {
error = validateFormFields(name, value)!;
}
setValues((prevState) => ({ ...prevState, [name]: value }));
setErrors((prevState) => ({ ...prevState, [name]: error }));
isValid((prevState) => ({ ...prevState, [name]: true }));
},
[validateFormFields],
);
const handleOnSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Making sure that there's no error in the state
// before calling the submit callback function
// and disabled button
if (!validateErrorState()) {
submitFormCallback(values);
setDisable(true);
}
},
[validateErrorState, submitFormCallback, values],
);
return {
handleOnChange,
handleOnSubmit,
values,
errors,
disable,
setValues,
setErrors,
valid,
};
};
export default useForm;
+123 -260
View File
@@ -1,11 +1,20 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from '../../commons/styles/Button.ts'; import Button from '../../commons/styles/Button.ts';
// Types // Types
import { import { TRoom } from '../../globals/types.ts';
TRoom, import { useForm } from 'react-hook-form';
} from '../../globals/types.ts'; import { ROOM_PATH } from '../../constants/path.ts';
import { STATUS_CODE } from '../../constants/statusCode.ts';
import toast from 'react-hot-toast';
import { ADD_SUCCESS, EDIT_SUCCESS, errorMsg } from '../../constants/messages.ts';
import { sendRequest } from '../../helpers/sendRequest.ts';
import Input from '../../commons/styles/Input.ts';
import FormRow from '../../components/LabelControl/index.tsx';
import { INVALID_DISCOUNT, INVALID_FIELD, REQUIRED_FIELD_ERROR } from '../../constants/formValidateMessage.ts';
import { isValidNumber, isValidString } from '../../helpers/validators.ts';
import Form from '../../components/Form/index.tsx';
import { useEffect } from 'react';
const FormBtn = styled(Button)` const FormBtn = styled(Button)`
width: 100%; width: 100%;
@@ -32,272 +41,126 @@ const RoomForm = ({
room, room,
isAdd, isAdd,
}: IRoomFormProp) => { }: IRoomFormProp) => {
// const [reset, setReset] = useState(true); const formMethods = useForm<TRoom>();
const {
register,
handleSubmit,
reset,
formState: { errors, isDirty, isValid },
trigger,
} = formMethods;
// if (isAdd) { useEffect(() => {
// // If this is add form, reset value if(room) {
// room = null; reset(room);
// } }
}, [room, reset]);
// // prettier-ignore // Submit form
// const initialValue: string = isAdd const onSubmit = async (room: TRoom) => {
// ? '' try {
// : room! if (isAdd) {
// && room.id.toString(); // Add request
const response = await sendRequest(
ROOM_PATH,
JSON.stringify(room),
'POST'
);
// const { if (response.statusCode === STATUS_CODE.CREATE) {
// idValue, toast.success(ADD_SUCCESS);
// nameValue, } else {
// priceValue, throw new Error(errorMsg(response.statusCode, response.msg));
// discountValue, }
// statusValue, } else {
// descriptionValue, // Edit request
// } = getValueFromObj<TRoom>(room); const response = await sendRequest(
ROOM_PATH + `/${room!.id}`,
JSON.stringify(room),
'PUT'
);
// // Define your state schema if (response.statusCode == STATUS_CODE.OK) {
// // prettier-ignore toast.success(EDIT_SUCCESS);
// const stateSchema: TStateSchema = { } else {
// id: { throw new Error(errorMsg(response.statusCode, response.msg));
// value: idValue || '' }
// }, }
// name: { // Reload table data
// value: nameValue || '', setReload(!reload);
// error: '' } catch (error: unknown) {
// }, if (error instanceof Error) {
// price: { toast.error(error.message);
// value: priceValue || '', }
// error: '' }
// },
// discount: {
// value: discountValue || '',
// error: ''
// },
// status: {
// value: statusValue || '',
// error: ''
// },
// description: {
// value: descriptionValue || '',
// error: ''
// },
// };
// // prettier-ignore reset();
// const stateValidatorSchema: TValidator = { onClose();
// name: addValidator({ };
// validatorFunc: isValidString,
// prop: 'name'
// }),
// price: addValidator({
// validatorFunc: isValidNumber,
// prop: 'price',
// }),
// discount: addValidator({
// validatorFunc: isValidDiscount,
// customErrorMsg: DISCOUNT_FIELD_ERROR
// }),
// status: addValidator({
// validatorFunc: skipCheck,
// prop: 'status',
// required: false,
// }),
// description: addValidator({
// validatorFunc: isValidString,
// prop: 'description'
// }),
// };
// // Submit form
// const onSubmitForm = async (state: TKeyValue) => {
// // Convert to room type
// const data: TRoom = {
// id: +state.id!,
// name: '' + state.name,
// discount: +state.discount!,
// price: +state.price!,
// status: !!state.status,
// description: '' + state.description,
// };
// try {
// if (isAdd) {
// // Add request
// const response = await sendRequest(
// ROOM_PATH,
// JSON.stringify(data),
// 'POST',
// );
// if (response.statusCode === STATUS_CODE.CREATE) {
// toast.success(ADD_SUCCESS);
// onResetForm();
// } else {
// throw new Error(errorMsg(response.statusCode, response.msg));
// }
// } else {
// // Edit request
// const response = await sendRequest(
// ROOM_PATH + `/${room!.id}`,
// JSON.stringify(data),
// 'PUT',
// );
// if (response.statusCode == STATUS_CODE.OK) {
// toast.success(EDIT_SUCCESS);
// } else {
// throw new Error(errorMsg(response.statusCode, response.msg));
// }
// }
// // Reload table data
// setReload(!reload);
// } catch (error: unknown) {
// if (error instanceof Error) {
// toast.error(error.message);
// }
// }
// onClose();
// };
// // Close and reset form
// const closeAndReset = () => {
// onClose();
// onResetForm();
// };
// // prettier-ignore
// const {
// values,
// errors,
// valid,
// handleOnChange,
// handleOnSubmit,
// disable } =
// useForm(
// stateSchema,
// stateValidatorSchema,
// onSubmitForm,
// initialValue
// );
// // prettier-ignore
// const {
// id,
// name,
// price,
// discount,
// status,
// description
// } = values;
// // Clear form value when this is a add form
// useEffect(() => {
// if (isAdd) {
// Object.keys(values).forEach((key) => (values[key] = ''));
// }
// }, [isAdd]); // eslint-disable-line
// // Reset form
// const onResetForm = () => {
// setReset(!reset);
// Object.keys(values).forEach((key) => (values[key] = ''));
// };
return ( return (
// <Form onSubmit={handleOnSubmit}> <Form onSubmit={handleSubmit(onSubmit)}>
// <Input type="hidden" name="id" value={id as string} /> <Input type="hidden" id="id" {...register('id')} />
// <FormRow <FormRow label="Name" error={errors?.name?.message}>
// label="Name" <Input
// error={ type="text"
// // prettier-ignore id="name"
// errors.name && valid.name {...register('name', {
// ? (errors.name as string) required: REQUIRED_FIELD_ERROR,
// : '' validate: {
// } checkValidName: (value) =>
// > isValidString(value) || INVALID_FIELD,
// <Input },
// type="text" onChange: () => trigger('name'),
// name="name" })}
// value={name as string} />
// onChange={handleOnChange} </FormRow>
// />
// </FormRow>
// <FormRow
// label="Price"
// error={
// // prettier-ignore
// errors.price && valid.price
// ? (errors.price as string)
// : ''
// }
// >
// <Input
// type="text"
// name="price"
// value={price as string}
// onChange={handleOnChange}
// />
// </FormRow>
// <FormRow <FormRow
// label="Discount" label="Price"
// error={ error={errors?.price?.message}
// // prettier-ignore >
// errors.discount && valid.discount <Input
// ? (errors.discount as string) type="text"
// : '' id="price"
// } {...register('price', {
// > required: REQUIRED_FIELD_ERROR,
// <Input validate: {
// type="text" checkIdentifiedCode: (v) => isValidNumber(v) || INVALID_FIELD,
// name="discount" },
// value={discount as string} onChange: () => trigger('price'),
// onChange={handleOnChange} })}
// /> />
// </FormRow> </FormRow>
// <FormRow label="Status"> <FormRow label="discount" error={errors?.discount?.message}>
// <Input <Input
// type="checkbox" type="text"
// name="status" id="phone"
// checked={status as boolean} {...register('discount', {
// onChange={handleOnChange} required: REQUIRED_FIELD_ERROR,
// /> validate: {
// </FormRow> checkPhoneNum: (v) => isValidNumber(v) || INVALID_DISCOUNT,
},
onChange: () => trigger('discount'),
})}
/>
</FormRow>
// <FormRow <Form.Action>
// label="Description" <FormBtn type="submit" name="submit" disabled={!isDirty || !isValid}>
// error={ {
// // prettier-ignore // prettier-ignore
// errors.description && valid.description isAdd
// ? (errors.description as string) ? 'Add'
// : '' : 'Save'
// } }
// > </FormBtn>
// <TextArea <FormBtn type="button" styled="secondary" onClick={onClose}>
// name="description" Close
// rows={3} </FormBtn>
// value={description as string} </Form.Action>
// onChange={handleOnChange} </Form>
// />
// </FormRow>
// <Form.Action>
// <FormBtn type="submit" name="submit" disabled={disable}>
// {
// // prettier-ignore
// isAdd
// ? 'Add'
// : 'Save'
// }
// </FormBtn>
// <FormBtn type="button" styled="secondary" onClick={closeAndReset}>
// Close
// </FormBtn>
// </Form.Action>
// </Form>
<p></p>
); );
}; };
+2 -4
View File
@@ -67,7 +67,7 @@ const RoomRow = ({
} }
}; };
const { id, name, price, discount, status } = room; const { id, name, price, status } = room;
// prettier-ignore // prettier-ignore
const statusText = status const statusText = status
@@ -79,7 +79,6 @@ const RoomRow = ({
<div>{id}</div> <div>{id}</div>
<div>{name}</div> <div>{name}</div>
<div>{price}</div> <div>{price}</div>
<div>{discount}</div>
<div>{statusText}</div> <div>{statusText}</div>
<Menus.Menu> <Menus.Menu>
@@ -162,12 +161,11 @@ const RoomTable = ({
{rooms.length ? ( {rooms.length ? (
<Menus> <Menus>
<Table columns="10% 30% 20% 10% 20% 5%"> <Table columns="10% 40% 20% 20% 5%">
<Table.Header> <Table.Header>
<div>Id</div> <div>Id</div>
<div>Name</div> <div>Name</div>
<div>Price</div> <div>Price</div>
<div>Discount</div>
<div>Status</div> <div>Status</div>
</Table.Header> </Table.Header>
<Table.Body<TRoom> <Table.Body<TRoom>
+3 -2
View File
@@ -111,6 +111,7 @@ const UserForm = ({
} }
// TODO Update status when user create // TODO Update status when user create
} else { } else {
// Edit request // Edit request
const response = await sendRequest( const response = await sendRequest(
@@ -190,7 +191,7 @@ const UserForm = ({
</FormRow> </FormRow>
<FormRow label="Room"> <FormRow label="Room">
<Select id="roomId" options={options!} /> <Select id="roomId" options={options!} ariaLabel='RoomId'/>
</FormRow> </FormRow>
<FormRow label="Address" error={errors.address?.message}> <FormRow label="Address" error={errors.address?.message}>
@@ -217,7 +218,7 @@ const UserForm = ({
: 'Save' : 'Save'
} }
</FormBtn> </FormBtn>
<FormBtn type="button" styled="secondary"> <FormBtn type="button" styled="secondary" onClick={onClose}>
Close Close
</FormBtn> </FormBtn>
</Form.Action> </Form.Action>