In a real project, facing the form validation soon begin on frontend coding. After several rounds of refactoring, I completed it with 4 points in my project:
- Totally TypeScript
- Speed up development with depository support
- Custom hook
- Minimum refactoring for components
Chose Yup for validation schema definition, it is simple and easy to understand:
https://github.com/jquense/yup
npm install -S yup npm install -D @types/yup
React custom hook is a common function, with parameters for input and return necessary tool methods. useFormValidator as below is a custom hook and only rely on packages "react" and "yup", no relationship with the Material-UI framework:
import React from "react" import * as Yup from 'yup' /** * Form validator state field */ interface FormValidatorStateField { /** * Is error state */ error: boolean /** * state text */ text: string } /** * Form validator state fields */ interface FormValidatorStateFields { [key: string]: FormValidatorStateField } /** * Form validatior * @param schemas Initial validation schemas * @param milliseconds Merge change update interval */ export const useFormValidator = (schemas: Yup.ObjectSchema<object>, milliseconds: number = 200) => { // useState init const defaultState: FormValidatorStateFields = {} const [state, updateState] = React.useState<FormValidatorStateFields>(defaultState) // Change timeout seed let changeSeed = 0 // Change value handler const commitChange = (field: string, value: any) => { // Validate the field, then before catch, if catch before then, both will be triggered Yup.reach(schemas, field).validate(value).then(result => { commitResult(field, result) }).catch(result => { commitResult(field, result) }) } // Commit state result const commitResult = (field: string, result: any) => { let currentItem = state[field] if(result instanceof Yup.ValidationError) { // Error if(currentItem) { // First to avoid same result redraw if(currentItem.error && currentItem.text == result.message) return // Update state currentItem.error = true currentItem.text = result.message } else { // New item const newItem: FormValidatorStateField = { error: true, text: result.message } state[field] = newItem } } else { // Success and no result, just continue if(currentItem == null) return // Delete current state result delete state[field] } // Update state, for object update, need a clone const newState = {...state} updateState(newState) } // Clear timeout seed const clearSeed = () => { if(changeSeed > 0) clearTimeout(changeSeed) } // Delay change const delayChange = (field: string, value: any) => { clearSeed() changeSeed = setTimeout(() => { commitChange(field, value) }, milliseconds) } // Merge into the life cycle React.useEffect(() => { return () => { // clearTimeout before dispose the view clearSeed() } }, []) // Return methods for manipulation return { /** * Input or Textarea blur handler * @param event Focus event */ blurHandler: (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = event.currentTarget delayChange(name, value) }, /** * Input or Textarea change handler * @param event Change event */ changeHandler: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = event.currentTarget delayChange(name, value) }, /** * Commit change */ commitChange: commitChange, /** * State error or not * @param field Field name */ errors: (field: string) => { return state[field]?.error }, /** * State text * @param field Field name */ texts: (field: string) => { return state[field]?.text }, /** * Validate form data * @param data form data, Object.fromEntries(new FormData(form)) */ validate: async (data: any) => { try { clearSeed() return await schemas.validate(data, { strict: true, abortEarly: false, stripUnknown: false }) } catch(e) { // Reset const newState: FormValidatorStateFields = {} // Iterate the error items if(e instanceof Yup.ValidationError) { for(let error of e.inner) { // Only show the first error of the field if(newState[error.path] == null) { // New item const newItem: FormValidatorStateField = { error: true, text: error.message } newState[error.path] = newItem } } } // Update state updateState(newState) } return null } } }
When use it in Materal-UI pages, for example a login page:
// Login component function Login() { // Form validator const { blurHandler, changeHandler, errors, texts, validate } = useFormValidator(validationSchemas) // Login action async function doLogin(event: React.FormEvent<HTMLFormElement>) { // Prevent default action event.preventDefault() // Form JSON data let data = await validate(Object.fromEntries(new FormData(event.currentTarget))) if(data == null) return // Local data format // Parase as model const model = data as LoginModel } return ( <Container component="main" maxWidth="xs"> <CssBaseline /> <img src={window.location.origin + '/logo.jpg'} alt="Logo" className={classes.logo}/> <div className={classes.paper}> <Avatar className={classes.avatar}> <LockOutlined /> </Avatar> <Typography component="h1" variant="h5"> Sign in </Typography> <form className={classes.form} onSubmit={doLogin} noValidate> <TextField variant="outlined" margin="normal" required fullWidth id="id" label="Id or Email" name="id" error={errors('id')} helperText={texts('id')} onChange={changeHandler} onBlur={blurHandler} autoComplete="email" autoFocus /> <TextField variant="outlined" margin="normal" type="password" required fullWidth name="password" error={errors('password')} helperText={texts('password')} onChange={changeHandler} onBlur={blurHandler} label="Password" id="password" autoComplete="current-password" /> <FormControlLabel control={<Checkbox name="save" value="true" color="primary" />} label="Remember me" /> <Button type="submit" fullWidth variant="contained" color="primary" className={classes.submit} > Sign In </Button> </form> </div> </Container> )
First declear the validation schemas, initialize 'useFormValidator' and accept the returned methods for binding:
error={errors('password')} helperText={texts('password')} onChange={changeHandler} onBlur={blurHandler}
Through bindings only to current components to indicate any validation errors occur. No refactoring or extending for current components. That's the key feature of the task I enjoyed.
Top comments (2)
This is the ugliest article I have seen in a long time, no code format, no explanations, long files with few comments...
It's a rush for a reference/guide during my interview. Sorry about that. I am working on a repository of the source codes, maybe later will much helpful.