mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 20:01:59 +00:00
Merge pull request #11 from Nez27/feat/add-form-component
Add form, dialog component and useForm hooks
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { IDialogProps } from '../../globals/interfaces';
|
||||||
|
|
||||||
|
// Styled
|
||||||
|
import { StyledBody, StyledDialog, StyledTitle } from './styled';
|
||||||
|
|
||||||
|
const Dialog = forwardRef((props, ref) => {
|
||||||
|
const { title, children, onClose } = props as IDialogProps;
|
||||||
|
return (
|
||||||
|
<StyledDialog
|
||||||
|
ref={ref as React.LegacyRef<HTMLDialogElement> | undefined}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<StyledTitle>{title}</StyledTitle>
|
||||||
|
<StyledBody>{children}</StyledBody>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
}) as React.FC<IDialogProps>;
|
||||||
|
|
||||||
|
export default Dialog;
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { StyledActionBtn, StyledForm } from './styled';
|
||||||
|
|
||||||
|
interface IFormProps {
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Form = ({ children, onSubmit, id }: IFormProps) => {
|
||||||
|
return (
|
||||||
|
<StyledForm onSubmit={onSubmit} id={id}>
|
||||||
|
{children}
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Form.Action = StyledActionBtn;
|
||||||
|
|
||||||
|
export default Form;
|
||||||
@@ -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 };
|
||||||
@@ -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 (
|
||||||
|
<StyledFormRow>
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
{error && <Error>{error}</Error>}
|
||||||
|
</div>
|
||||||
|
</StyledFormRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormRow;
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useState, useEffect, useCallback, ChangeEvent } from 'react';
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import {
|
||||||
|
ERROR,
|
||||||
|
VALUE,
|
||||||
|
getPropValues,
|
||||||
|
isObject,
|
||||||
|
isRequired,
|
||||||
|
} from '../helpers/utils';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
const useForm = (
|
||||||
|
stateSchema = {},
|
||||||
|
stateValidatorSchema = {} as TValidator,
|
||||||
|
submitFormCallback: (values: TKeyValue) => void,
|
||||||
|
) => {
|
||||||
|
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(() => {
|
||||||
|
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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
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<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Making sure that there's no error in the state
|
||||||
|
// before calling the submit callback function
|
||||||
|
if (!validateErrorState()) {
|
||||||
|
submitFormCallback(values);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[validateErrorState, submitFormCallback, values],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleOnChange,
|
||||||
|
handleOnSubmit,
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
disable,
|
||||||
|
setValues,
|
||||||
|
setErrors,
|
||||||
|
dirty,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useForm;
|
||||||
Reference in New Issue
Block a user