From 5641c983a471977b089ad16f63f5dd9f80ae20b1 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 26 Oct 2023 17:31:32 +0700 Subject: [PATCH 1/2] Add form, dialog component and useForm hooks --- .../src/components/Dialog/index.tsx | 18 +++ .../src/components/Dialog/styled.ts | 23 +++ .../src/components/Form/index.tsx | 19 +++ .../src/components/Form/styled.ts | 16 ++ .../src/components/FormRow/index.tsx | 21 +++ .../src/components/FormRow/styled.ts | 21 +++ hotel-management/src/hooks/useForm.ts | 139 ++++++++++++++++++ 7 files changed, 257 insertions(+) create mode 100644 hotel-management/src/components/Dialog/index.tsx create mode 100644 hotel-management/src/components/Dialog/styled.ts create mode 100644 hotel-management/src/components/Form/index.tsx create mode 100644 hotel-management/src/components/Form/styled.ts create mode 100644 hotel-management/src/components/FormRow/index.tsx create mode 100644 hotel-management/src/components/FormRow/styled.ts create mode 100644 hotel-management/src/hooks/useForm.ts diff --git a/hotel-management/src/components/Dialog/index.tsx b/hotel-management/src/components/Dialog/index.tsx new file mode 100644 index 0000000..1c9c23f --- /dev/null +++ b/hotel-management/src/components/Dialog/index.tsx @@ -0,0 +1,18 @@ +import { forwardRef } from 'react'; +import { IDialogProps } from '../../globals/interfaces'; +import { StyledBody, StyledDialog, StyledTitle } from './styled'; + +const Dialog = forwardRef((props, ref) => { + const { title, children, onClose } = props as IDialogProps; + return ( + | undefined} + onClose={onClose} + > + {title} + {children} + + ); +}) as React.FC; + +export default Dialog; diff --git a/hotel-management/src/components/Dialog/styled.ts b/hotel-management/src/components/Dialog/styled.ts new file mode 100644 index 0000000..700551b --- /dev/null +++ b/hotel-management/src/components/Dialog/styled.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +const StyledDialog = styled.dialog` + border-radius: var(--radius-sm); + border-color: var(--border-color); +`; + +const StyledTitle = styled.p` + font-size: var(--fs-md); + text-transform: capitalize; + font-weight: 600; + + padding: 20px 40px; + border-bottom: 1px solid var(--border-color); + + text-align: center; +`; + +const StyledBody = styled.div` + padding: 10px 20px; +`; + +export { StyledDialog, StyledTitle, StyledBody }; diff --git a/hotel-management/src/components/Form/index.tsx b/hotel-management/src/components/Form/index.tsx new file mode 100644 index 0000000..0d2de0b --- /dev/null +++ b/hotel-management/src/components/Form/index.tsx @@ -0,0 +1,19 @@ +import { StyledActionBtn, StyledForm } from './styled'; + +interface IFormProps { + children: JSX.Element | JSX.Element[]; + onSubmit: (event: React.FormEvent) => void; + id?: string; +} + +const Form = ({ children, onSubmit, id }: IFormProps) => { + return ( + + {children} + + ); +}; + +Form.Action = StyledActionBtn; + +export default Form; diff --git a/hotel-management/src/components/Form/styled.ts b/hotel-management/src/components/Form/styled.ts new file mode 100644 index 0000000..bc154c3 --- /dev/null +++ b/hotel-management/src/components/Form/styled.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +const StyledForm = styled.form` + display: flex; + flex-direction: column; + gap: 15px; +`; + +const StyledActionBtn = styled.div` + display: flex; + justify-content: space-around; + flex-direction: row; + gap: 100px; +`; + +export { StyledForm, StyledActionBtn }; diff --git a/hotel-management/src/components/FormRow/index.tsx b/hotel-management/src/components/FormRow/index.tsx new file mode 100644 index 0000000..3a4a1f5 --- /dev/null +++ b/hotel-management/src/components/FormRow/index.tsx @@ -0,0 +1,21 @@ +import { Error, Label, StyledFormRow } from './styled'; + +interface IFormRow { + label: string; + error?: string; + children: JSX.Element | JSX.Element[]; +} + +const FormRow = ({ label, error, children }: IFormRow) => { + return ( + + +
+ {children} + {error && {error}} +
+
+ ); +}; + +export default FormRow; diff --git a/hotel-management/src/components/FormRow/styled.ts b/hotel-management/src/components/FormRow/styled.ts new file mode 100644 index 0000000..6563746 --- /dev/null +++ b/hotel-management/src/components/FormRow/styled.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +const StyledFormRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 100px; +`; + +const Label = styled.label` + font-size: var(--fs-sm); + text-transform: capitalize; +`; + +const Error = styled.p` + color: var(--error-text); + font-size: var(--fs-sm-2x); + margin-top: 5px; +`; + +export { StyledFormRow, Label, Error }; diff --git a/hotel-management/src/hooks/useForm.ts b/hotel-management/src/hooks/useForm.ts new file mode 100644 index 0000000..54e4ad0 --- /dev/null +++ b/hotel-management/src/hooks/useForm.ts @@ -0,0 +1,139 @@ +import { useState, useEffect, useCallback, ChangeEvent } from 'react'; +import { + ERROR, + VALUE, + getPropValues, + isObject, + isRequired, +} from '../helpers/utils'; +import { TKeyValue, TValidator } from '../globals/types'; + +/** + * 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 + */ +function useForm( + stateSchema = {}, + stateValidatorSchema = {} as TValidator, + submitFormCallback: (values: TKeyValue) => void, +) { + const [state, setStateSchema] = useState(stateSchema); + + const [values, setValues] = useState(getPropValues(state, VALUE)); + const [errors, setErrors] = useState(getPropValues(state, ERROR)); + const [dirty, setDirty] = useState(getPropValues(state)); + + const [disable, setDisable] = useState(true); + const [isDirty, setIsDirty] = useState(false); + + // Get a local copy of stateSchema + useEffect(() => { + setStateSchema(stateSchema); + setDisable(true); // Disable button in initial render. + setInitialErrorState(); + }, []); // 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 = ''; + 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(() => { + Object.keys(errors).map((name) => + setErrors((prevState) => ({ + ...prevState, + [name]: 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) => { + setIsDirty(true); + + const name = (event.target! as HTMLInputElement).name; + const value = (event.target! as HTMLInputElement).value; + + const error = validateFormFields(name, value); + + setValues((prevState) => ({ ...prevState, [name]: value })); + setErrors((prevState) => ({ ...prevState, [name]: error })); + setDirty((prevState) => ({ ...prevState, [name]: true })); + }, + [validateFormFields], + ); + + const handleOnSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + + // Making sure that there's no error in the state + // before calling the submit callback function + if (!validateErrorState()) { + submitFormCallback(values); + console.log(values); + } + }, + [validateErrorState, submitFormCallback, values], + ); + + return { + handleOnChange, + handleOnSubmit, + values, + errors, + disable, + setValues, + setErrors, + dirty, + }; +} + +export default useForm; From af2b169926dd8266f9578c17097f29ebe3c280e1 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 26 Oct 2023 20:43:50 +0700 Subject: [PATCH 2/2] Fix code style format --- .../src/components/Dialog/index.tsx | 4 ++++ hotel-management/src/hooks/useForm.ts | 21 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/hotel-management/src/components/Dialog/index.tsx b/hotel-management/src/components/Dialog/index.tsx index 1c9c23f..8e73da6 100644 --- a/hotel-management/src/components/Dialog/index.tsx +++ b/hotel-management/src/components/Dialog/index.tsx @@ -1,5 +1,9 @@ import { forwardRef } from 'react'; + +// Components import { IDialogProps } from '../../globals/interfaces'; + +// Styled import { StyledBody, StyledDialog, StyledTitle } from './styled'; const Dialog = forwardRef((props, ref) => { diff --git a/hotel-management/src/hooks/useForm.ts b/hotel-management/src/hooks/useForm.ts index 54e4ad0..74be96e 100644 --- a/hotel-management/src/hooks/useForm.ts +++ b/hotel-management/src/hooks/useForm.ts @@ -1,4 +1,6 @@ import { useState, useEffect, useCallback, ChangeEvent } from 'react'; + +// Utils import { ERROR, VALUE, @@ -6,6 +8,8 @@ import { isObject, isRequired, } from '../helpers/utils'; + +// Types import { TKeyValue, TValidator } from '../globals/types'; /** @@ -16,23 +20,19 @@ import { TKeyValue, TValidator } from '../globals/types'; * @param submitFormCallback function to be execute during form submission. * @returns */ -function useForm( +const useForm = ( stateSchema = {}, stateValidatorSchema = {} as TValidator, submitFormCallback: (values: TKeyValue) => void, -) { - const [state, setStateSchema] = useState(stateSchema); - - const [values, setValues] = useState(getPropValues(state, VALUE)); - const [errors, setErrors] = useState(getPropValues(state, ERROR)); - const [dirty, setDirty] = useState(getPropValues(state)); - +) => { + const [values, setValues] = useState(getPropValues(stateSchema, VALUE)); + const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR)); + const [dirty, setDirty] = useState(getPropValues(stateSchema)); const [disable, setDisable] = useState(true); const [isDirty, setIsDirty] = useState(false); // Get a local copy of stateSchema useEffect(() => { - setStateSchema(stateSchema); setDisable(true); // Disable button in initial render. setInitialErrorState(); }, []); // eslint-disable-line @@ -118,7 +118,6 @@ function useForm( // before calling the submit callback function if (!validateErrorState()) { submitFormCallback(values); - console.log(values); } }, [validateErrorState, submitFormCallback, values], @@ -134,6 +133,6 @@ function useForm( setErrors, dirty, }; -} +}; export default useForm;