Apply react hook form for user page

This commit is contained in:
2023-11-03 17:30:34 +07:00
parent ae7c25e6f3
commit 32a65038f5
10 changed files with 404 additions and 577 deletions
@@ -1,3 +1,4 @@
import { FieldValues, RegisterOptions, useFormContext } from 'react-hook-form';
import { StyledSelect } from './styled';
export interface ISelectOptions {
@@ -6,20 +7,25 @@ export interface ISelectOptions {
}
interface ISelect {
options: ISelectOptions[];
value: string;
onChange: React.ChangeEventHandler<HTMLSelectElement>;
name?: string;
optionsConfigForm?: RegisterOptions<FieldValues, string> | undefined;
value?: number;
id: string;
}
const Select = ({ options, value, onChange, name }: ISelect) => {
const Select = ({ options, id, optionsConfigForm }: ISelect) => {
const { register } = useFormContext() ?? {};
if(!register) {
return null;
}
if (!options) return;
return (
<StyledSelect
value={value}
onChange={onChange}
aria-label="Sort"
name={name}
id={id}
{...register(id, optionsConfigForm)}
>
{options.map((option) => (
<option value={option.value} key={option.value}>
@@ -0,0 +1,6 @@
const INVALID_FIELD = 'This field is invalid format!';
const INVALID_PHONE = 'Phone number is invalid!';
const REQUIRED_FIELD_ERROR = 'This field is required!';
const INVALID_DISCOUNT = 'Discount should be greater than 0 and less than 100!';
export { REQUIRED_FIELD_ERROR, INVALID_DISCOUNT, INVALID_FIELD, INVALID_PHONE };
@@ -1,7 +1,3 @@
const invalidFormatMsg = (field: string) => {
return `Invalid ${field} format`;
};
const errorMsg = (errorCode: number, msg: string) => {
return `Error code: ${errorCode}. Message: ${msg}`;
};
@@ -10,17 +6,11 @@ const ADD_SUCCESS = 'Add success';
const EDIT_SUCCESS = 'Edit success';
const CONFIRM_DELETE = 'Are you sure to delete it?';
const DELETE_SUCCESS = 'Delete success';
const REQUIRED_FIELD_ERROR = 'This is required field';
const DISCOUNT_FIELD_ERROR =
'Discount should be greater than 0 and less than 100';
export {
invalidFormatMsg,
ADD_SUCCESS,
errorMsg,
EDIT_SUCCESS,
CONFIRM_DELETE,
DELETE_SUCCESS,
REQUIRED_FIELD_ERROR,
DISCOUNT_FIELD_ERROR,
};
+1 -1
View File
@@ -2,6 +2,6 @@ export const DASHBOARD = '/dashboard';
export const USER = '/user';
export const ROOM = '/room';
export const OTHER_PATH = '*';
export const BASE_URL = 'https://hotel-management-api.loiphan.com/';
export const BASE_URL = 'http://localhost:3000/';
export const USER_PATH = 'users';
export const ROOM_PATH = 'rooms';
+1 -32
View File
@@ -1,5 +1,5 @@
// Constants
import { REQUIRED_FIELD_ERROR, invalidFormatMsg } from '../constants/messages';
import { REQUIRED_FIELD_ERROR } from '../constants/formValidateMessage';
// Types
import {
@@ -54,36 +54,6 @@ const getPropValues = (stateSchema: TStateSchema, prop?: TPropValues) => {
}, {} as TKeyValue);
};
type TValidator = {
validatorFunc: (value: string) => boolean;
prop?: string;
customErrorMsg?: string;
required?: boolean;
};
/**
* Create validator object
* @param param0 Pass TValidator object
* @returns An object contains condition validator
*/
const addValidator = ({
validatorFunc,
prop = '',
customErrorMsg = '',
required = true,
}: TValidator) => {
return {
required,
validator: {
func: validatorFunc,
// prettier-ignore
error: customErrorMsg
? customErrorMsg
: invalidFormatMsg(prop),
},
};
};
/**
* Return the object contains values of object pass
* @param obj Object need to get value
@@ -163,7 +133,6 @@ export {
isRequired,
getPropValues,
getValueFromObj,
addValidator,
searchQuery,
REQUIRED_FIELD_ERROR,
};
+245 -279
View File
@@ -1,47 +1,12 @@
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import toast from 'react-hot-toast';
// Styled
import Input from '../../commons/styles/Input.ts';
import TextArea from '../../commons/styles/TextArea.ts';
// Components
import Form from '../../components/Form/index.tsx';
import FormRow from '../../components/LabelControl/index.tsx';
import Button from '../../commons/styles/Button.ts';
// Types
import {
TKeyValue,
TRoom,
TStateSchema,
TValidator,
} from '../../globals/types.ts';
// Hooks
import useForm from '../../hooks/useForm.ts';
// Utils
import {
isValidDiscount,
isValidNumber,
isValidString,
skipCheck,
} from '../../helpers/validators.ts';
import { addValidator, getValueFromObj } from '../../helpers/utils.ts';
import { sendRequest } from '../../helpers/sendRequest.ts';
// Constants
import { STATUS_CODE } from '../../constants/statusCode.ts';
import {
ADD_SUCCESS,
DISCOUNT_FIELD_ERROR,
EDIT_SUCCESS,
errorMsg,
} from '../../constants/messages.ts';
import { ROOM_PATH } from '../../constants/path.ts';
const FormBtn = styled(Button)`
width: 100%;
@@ -67,271 +32,272 @@ const RoomForm = ({
room,
isAdd,
}: IRoomFormProp) => {
const [reset, setReset] = useState(true);
// const [reset, setReset] = useState(true);
if (isAdd) {
// If this is add form, reset value
room = null;
}
// if (isAdd) {
// // If this is add form, reset value
// room = null;
// }
// prettier-ignore
const initialValue: string = isAdd
? ''
: room!
&& room.id.toString();
// // prettier-ignore
// const initialValue: string = isAdd
// ? ''
// : room!
// && room.id.toString();
const {
idValue,
nameValue,
priceValue,
discountValue,
statusValue,
descriptionValue,
} = getValueFromObj<TRoom>(room);
// const {
// idValue,
// nameValue,
// priceValue,
// discountValue,
// statusValue,
// descriptionValue,
// } = getValueFromObj<TRoom>(room);
// Define your state schema
// prettier-ignore
const stateSchema: TStateSchema = {
id: {
value: idValue || ''
},
name: {
value: nameValue || '',
error: ''
},
price: {
value: priceValue || '',
error: ''
},
discount: {
value: discountValue || '',
error: ''
},
status: {
value: statusValue || '',
error: ''
},
description: {
value: descriptionValue || '',
error: ''
},
};
// // Define your state schema
// // prettier-ignore
// const stateSchema: TStateSchema = {
// id: {
// value: idValue || ''
// },
// name: {
// value: nameValue || '',
// error: ''
// },
// price: {
// value: priceValue || '',
// error: ''
// },
// discount: {
// value: discountValue || '',
// error: ''
// },
// status: {
// value: statusValue || '',
// error: ''
// },
// description: {
// value: descriptionValue || '',
// error: ''
// },
// };
// prettier-ignore
const stateValidatorSchema: TValidator = {
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'
}),
};
// // prettier-ignore
// const stateValidatorSchema: TValidator = {
// 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,
};
// // 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',
);
// 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);
// 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',
);
// 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);
}
}
// 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();
};
// onClose();
// };
// Close and reset form
const closeAndReset = () => {
onClose();
onResetForm();
};
// // 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 {
// values,
// errors,
// valid,
// handleOnChange,
// handleOnSubmit,
// disable } =
// useForm(
// stateSchema,
// stateValidatorSchema,
// onSubmitForm,
// initialValue
// );
// prettier-ignore
const {
id,
name,
price,
discount,
status,
description
} = values;
// // 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
// // 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] = ''));
};
// // Reset form
// const onResetForm = () => {
// setReset(!reset);
// Object.keys(values).forEach((key) => (values[key] = ''));
// };
return (
<Form onSubmit={handleOnSubmit}>
<Input type="hidden" name="id" value={id as string} />
<FormRow
label="Name"
error={
// prettier-ignore
errors.name && valid.name
? (errors.name as string)
: ''
}
>
<Input
type="text"
name="name"
value={name as string}
onChange={handleOnChange}
/>
</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>
// <Form onSubmit={handleOnSubmit}>
// <Input type="hidden" name="id" value={id as string} />
// <FormRow
// label="Name"
// error={
// // prettier-ignore
// errors.name && valid.name
// ? (errors.name as string)
// : ''
// }
// >
// <Input
// type="text"
// name="name"
// value={name as string}
// onChange={handleOnChange}
// />
// </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
label="Discount"
error={
// prettier-ignore
errors.discount && valid.discount
? (errors.discount as string)
: ''
}
>
<Input
type="text"
name="discount"
value={discount as string}
onChange={handleOnChange}
/>
</FormRow>
// <FormRow
// label="Discount"
// error={
// // prettier-ignore
// errors.discount && valid.discount
// ? (errors.discount as string)
// : ''
// }
// >
// <Input
// type="text"
// name="discount"
// value={discount as string}
// onChange={handleOnChange}
// />
// </FormRow>
<FormRow label="Status">
<Input
type="checkbox"
name="status"
checked={status as boolean}
onChange={handleOnChange}
/>
</FormRow>
// <FormRow label="Status">
// <Input
// type="checkbox"
// name="status"
// checked={status as boolean}
// onChange={handleOnChange}
// />
// </FormRow>
<FormRow
label="Description"
error={
// prettier-ignore
errors.description && valid.description
? (errors.description as string)
: ''
}
>
<TextArea
name="description"
rows={3}
value={description as string}
onChange={handleOnChange}
/>
</FormRow>
// <FormRow
// label="Description"
// error={
// // prettier-ignore
// errors.description && valid.description
// ? (errors.description as string)
// : ''
// }
// >
// <TextArea
// name="description"
// rows={3}
// value={description as string}
// onChange={handleOnChange}
// />
// </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>
// <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>
);
};
+111 -247
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import toast from 'react-hot-toast';
import { FormProvider, useForm } from 'react-hook-form';
// Styled
import Input from '../../commons/styles/Input';
@@ -9,27 +9,17 @@ import TextArea from '../../commons/styles/TextArea';
// Components
import Form from '../../components/Form';
import FormRow from '../../components/LabelControl/index.tsx';
import Button from '../../commons/styles/Button.ts';
// Types
import {
TKeyValue,
TStateSchema,
TUser,
TValidator,
} from '../../globals/types';
// Hooks
import useForm from '../../hooks/useForm';
import { TKeyValue, TUser } from '../../globals/types';
// Utils
import { sendRequest } from '../../helpers/sendRequest.ts';
import {
isValidString,
isValidNumber,
isValidPhoneNumber,
} from '../../helpers/validators';
import { addValidator, getValueFromObj } from '../../helpers/utils.ts';
import { sendRequest } from '../../helpers/sendRequest.ts';
isValidString,
} from '../../helpers/validators.ts';
// Constants
import { STATUS_CODE } from '../../constants/statusCode.ts';
@@ -40,17 +30,13 @@ import {
} from '../../constants/messages.ts';
import { USER_PATH } from '../../constants/path.ts';
import Select, { ISelectOptions } from '../../components/Select';
// Hooks
import { useFetch } from '../../hooks/useFetch.ts';
const FormBtn = styled(Button)`
width: 100%;
&:disabled,
&[disabled] {
background-color: var(--disabled-btn-color);
cursor: no-drop;
}
`;
// Styled
import { FormBtn } from './styled.ts';
import { INVALID_FIELD, INVALID_PHONE, REQUIRED_FIELD_ERROR } from '../../constants/formValidateMessage.ts';
interface IUserFormProp {
onClose: () => void;
@@ -67,10 +53,27 @@ const UserForm = ({
user,
isAdd,
}: IUserFormProp) => {
const [reset, setReset] = useState(true);
const formMethods = useForm<TUser>({
defaultValues: {
roomId: 1,
},
});
const {
register,
handleSubmit,
reset,
formState: { errors, isDirty, isValid },
trigger,
} = formMethods;
const [options, setOptions] = useState<ISelectOptions[]>();
const { data, errorFetchMsg } = useFetch('rooms');
useEffect(() => {
if (user) {
reset(user);
}
}, [reset, user]);
useEffect(() => {
if (data) {
const tempData = data as TKeyValue[];
@@ -90,95 +93,15 @@ const UserForm = ({
}
}, [data, errorFetchMsg]);
if (isAdd) {
// If this is add form, reset value
user = null;
}
// prettier-ignore
const initialValue: string = isAdd
? ''
: user!
&& user.id.toLocaleString();
const {
idValue,
nameValue,
identifiedCodeValue,
phoneValue,
roomIdValue,
addressValue,
} = getValueFromObj<TUser>(user);
// Define your state schema
// prettier-ignore
const stateSchema: TStateSchema = {
id: { value: idValue || '' },
name: {
value: nameValue || '',
error: '' ,
},
identifiedCode: {
value: identifiedCodeValue || '',
error: '',
},
phone: {
value: phoneValue || '',
error: '',
},
roomId: {
value: roomIdValue || '' + (options && options[0].value),
error: '' ,
},
address: {
value: addressValue || '',
error: ''
},
};
// prettier-ignore
const stateValidatorSchema: TValidator = {
name: addValidator({
validatorFunc: isValidString,
prop: 'full name'
}),
identifiedCode: addValidator({
validatorFunc: isValidNumber,
prop: 'identified code',
}),
phone: addValidator({
validatorFunc: isValidPhoneNumber,
prop: 'phone number',
}),
roomId: addValidator({
validatorFunc: isValidNumber,
prop: 'room number'
}),
address: addValidator({
validatorFunc: isValidString,
prop: 'address'
}),
};
// Submit form
const onSubmitForm = async (state: TKeyValue) => {
// Convert to user type
const user: TUser = {
id: +state.id!,
name: '' + state.name,
identifiedCode: '' + state.identifiedCode,
phone: '' + state.phone,
roomId: +state.roomId!,
address: '' + state.address,
};
const onSubmit = async (user: TUser) => {
try {
if (isAdd) {
// Add request
const response = await sendRequest(
USER_PATH,
JSON.stringify(user),
'POST',
'POST'
);
if (response.statusCode === STATUS_CODE.CREATE) {
@@ -188,14 +111,12 @@ const UserForm = ({
}
// TODO Update status when user create
onResetForm();
} else {
// Edit request
const response = await sendRequest(
USER_PATH + `/${user!.id}`,
JSON.stringify(data),
'PUT',
JSON.stringify(user),
'PUT'
);
if (response.statusCode == STATUS_CODE.OK) {
@@ -212,153 +133,96 @@ const UserForm = ({
}
}
reset();
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,
identifiedCode,
phone,
roomId,
address
} = values;
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 (
<Form onSubmit={handleOnSubmit}>
<Input type="hidden" name="id" value={id as string} />
<FormRow
label="Full Name"
error={
// prettier-ignore
errors.name && valid.name
? (errors.name as string)
: ''
}
>
<Input
type="text"
name="name"
value={name as string}
onChange={handleOnChange}
/>
</FormRow>
<FormProvider {...formMethods}>
<Form onSubmit={handleSubmit(onSubmit)}>
<Input type="hidden" id="id" {...register('id')} />
<FormRow label="Full Name" error={errors?.name?.message}>
<Input
type="text"
id="name"
{...register('name', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkValidName: (value) =>
isValidString(value) || INVALID_FIELD,
},
onChange: () => trigger('name'),
})}
/>
</FormRow>
<FormRow
label="Identified Code"
error={
errors.identifiedCode && valid.identifiedCode
? (errors.identifiedCode as string)
: ''
}
>
<Input
type="text"
name="identifiedCode"
value={identifiedCode as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow
label="Identified Code"
error={errors?.identifiedCode?.message}
>
<Input
type="text"
id="identifiedCode"
{...register('identifiedCode', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkIdentifiedCode: (v) =>
isValidNumber(v) || INVALID_FIELD,
},
onChange: () => trigger('identifiedCode'),
})}
/>
</FormRow>
<FormRow
label="Phone"
error={
// prettier-ignore
errors.phone && valid.phone
? (errors.phone as string)
: ''
}
>
<Input
type="text"
name="phone"
value={phone as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow label="Phone" error={errors?.phone?.message}>
<Input
type="text"
id="phone"
{...register('phone', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkPhoneNum: (v) =>
isValidPhoneNumber(v) || INVALID_PHONE,
},
onChange: () => trigger('phone'),
})}
/>
</FormRow>
<FormRow
label="Room"
error={
// prettier-ignore
errors.roomId && valid.roomId
? (errors.roomId as string)
: ''
}
>
<Select
name="roomId"
value={roomId as string}
onChange={handleOnChange}
options={options!}
/>
</FormRow>
<FormRow label="Room">
<Select id="roomId" options={options!} />
</FormRow>
<FormRow
label="Address"
error={
// prettier-ignore
errors.address && valid.address
? (errors.address as string)
: ''
}
>
<TextArea
name="address"
rows={3}
value={address as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow label="Address" error={errors.address?.message}>
<TextArea
id="address"
rows={3}
{...register('address', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkValidString: (v) =>
isValidString(v) || INVALID_FIELD,
},
onChange: () => trigger('address'),
})}
/>
</FormRow>
<Form.Action>
<FormBtn type="submit" name="submit" disabled={disable}>
{
// prettier-ignore
isAdd
<Form.Action>
<FormBtn type="submit" name="submit" disabled={!isDirty || !isValid}>
{
// prettier-ignore
isAdd
? 'Add'
: 'Save'
}
</FormBtn>
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
Close
</FormBtn>
</Form.Action>
</Form>
}
</FormBtn>
<FormBtn type="button" styled="secondary">
Close
</FormBtn>
</Form.Action>
</Form>
</FormProvider>
);
};
+14 -1
View File
@@ -1,5 +1,8 @@
import styled from 'styled-components';
// Components
import Button from '../../commons/styles/Button';
const StyledUser = styled.main`
padding: 20px;
padding-bottom: 100px;
@@ -21,4 +24,14 @@ const StyledOperationTable = styled.div`
justify-content: flex-end;
`;
export { StyledUser, Title, StyledOperationTable };
const FormBtn = styled(Button)`
width: 100%;
&:disabled,
&[disabled] {
background-color: var(--disabled-btn-color);
cursor: no-drop;
}
`;
export { StyledUser, Title, StyledOperationTable, FormBtn };