Очистка React компонентов с помощью React Hook Form и Material UI

React Hook Form — одна из самых популярных библиотек для обработки элементов ввода формы в экосистеме React.
Но добиться ее правильной интеграции может быть непросто, если использовать какую-либо библиотеку компонентов.
Сегодня я покажу вам, как можно интегрировать React Hook Form с различными компонентами Material UI.
Предварительное условие
Я не буду подробно рассказывать о том, как использовать react-hook-form. Если же вы еще не знаете, как использовать react-hook-form, я настоятельно рекомендую вам сначала ознакомиться с этой статьей.
Все, что я могу сказать — вы не пожалеете о том, что изучили эту библиотеку.
Начальный код
Давайте посмотрим на код, с которого мы начнем.
import TextField from "@material-ui/core/TextField"; import React, { useState} from "react"; import { Button, Checkbox, FormControlLabel, FormLabel, MenuItem, Radio, RadioGroup, Select, Slider } from "@material-ui/core"; import {KeyboardDatePicker} from '@material-ui/pickers' const options = [ { label: 'Dropdown Option 1', value:'1' }, { label: 'Dropdown Option 2', value:'2' }, ] const radioOptions = [ { label: 'Radio Option 1', value:'1' }, { label: 'Radio Option 2', value:'2' }, ] const checkboxOptions = [ { label: 'Checkbox Option 1', value:'1' }, { label: 'Checkbox Option 2', value:'2' }, ] const DATE_FORMAT = 'dd-MMM-yy' export const FormBadDemo = () => { const [textValue , setTextValue] = useState(''); const [dropdownValue , setDropDownValue] = useState(''); const [sliderValue , setSliderValue] = useState(0); const [dateValue , setDateValue] = useState(new Date()); const [radioValue , setRadioValue] = useState(''); const [checkboxValue, setSelectedCheckboxValue] = useState<any>([]) const onTextChange = (e:any) => setTextValue(e.target.value) const onDropdownChange = (e:any) => setDropDownValue(e.target.value) const onSliderChange = (e:any) => setSliderValue(e.target.value) const onDateChange = (e:any) => setDateValue(e.target.value) const onRadioChange = (e:any) => setRadioValue(e.target.value) const handleSelect = (value:any) => { const isPresent = checkboxValue.indexOf(value) if (isPresent !== -1) { const remaining = checkboxValue.filter((item:any) => item !== value) setSelectedCheckboxValue(remaining) } else { setSelectedCheckboxValue((prevItems:any) => [...prevItems, value]) } } const handleSubmit = () => { console.log({ textValue: textValue, dropdownValue: dropdownValue, sliderValue: sliderValue, dateValue: dateValue, radioValue: radioValue, checkboxValue: checkboxValue, }) } const handleReset = () => { setTextValue('') setDropDownValue('') setSliderValue(0) setDateValue(new Date()) setRadioValue('') setSelectedCheckboxValue('') } return <form> <FormLabel component='legend'>Text Input</FormLabel> <TextField size='small' error={false} onChange={onTextChange} value={textValue} fullWidth label={'text Value'} variant='outlined' /> <FormLabel component='legend'>Dropdown Input</FormLabel> <Select id='site-select' inputProps={{ autoFocus: true }} value={dropdownValue} onChange={onDropdownChange} > {options.map((option: any) => { return ( <MenuItem key={option.value} value={option.value}> {option.label} </MenuItem> ) })} </Select> <FormLabel component='legend'>Slider Input</FormLabel> <Slider value={sliderValue} onChange={onSliderChange} valueLabelDisplay='auto' min={0} max={100} step={1} /> <FormLabel component='legend'>Date Input</FormLabel> <KeyboardDatePicker fullWidth variant='inline' defaultValue={new Date()} id={`date-${Math.random()}`} value={dateValue} onChange={onDateChange} rifmFormatter={(val) => val.replace(/[^[a-zA-Z0-9-]*$]+/gi, '')} refuse={/[^[a-zA-Z0-9-]*$]+/gi} autoOk KeyboardButtonProps={{ 'aria-label': 'change date' }} format={DATE_FORMAT} /> <FormLabel component='legend'>Radio Input</FormLabel> <RadioGroup aria-label='gender' value={radioValue} onChange={onRadioChange}> {radioOptions.map((singleItem) => ( <FormControlLabel value={singleItem.value} control={<Radio />} label={singleItem.label} /> ))} </RadioGroup> <FormLabel component='legend'>Checkbox Input</FormLabel> <div> {checkboxOptions.map(option => <Checkbox checked={checkboxValue.includes(option.value)} onChange={() => handleSelect(option.value)} /> )} </div> <Button onClick={handleSubmit} variant={'contained'} > Submit </Button> <Button onClick={handleReset} variant={'outlined'}> Reset </Button> </form> }
FormBadDemo.tsx
Это довольно стандартная форма. Мы использовали несколько наиболее распространенных элементов ввода формы. Но у данного компонента есть некоторые проблемы.
-
Обработчики
onChangeработают однообразно, в повторяющемся режиме. Если бы у нас было несколько текстовых элементов ввода, нам пришлось бы управлять ими по отдельности, а это так утомительно.
-
Когда нам нужно обрабатывать ошибки, то размеры и сложность таких операций возрастают.
Основная идея
Как вы знаете, react-hook-form отлично работает со стандартными компонентами ввода HTML. Однако все обстоит иначе, если мы используем различные библиотеки компонентов, такие как Material-UI, Ant design или любые другие.
Для таких случаев react-hook-form экспортирует специальный компонент-обертку под названием Controller . Если вам известно, как работает этот специальный компонент, то интегрировать его с любой другой библиотекой будет проще простого.
Структура компонента Controller выглядит следующим образом.
<Controller name={name} control={control} render={({ field: { onChange, value }}) => ( <AnyInputComponent onChange={onChange} value={value} /> )} />
Если вы занимались базовой обработкой форм, то знаете, что для любого компонента ввода важны два поля. Одно из них — value, а другое — onChange.
Поэтому наш компонент Controller инжектирует эти два свойства вместе со всем волшебным функционалом react-hook-form в компоненты.
Все остальное работает как по маслу! Давайте посмотрим на это в действии.
Пропсы элементов ввода формы
Каждому элементу ввода формы нужны два основных свойства — name и value. Эти 2 свойства управляют всеми функциональными возможностями.
Итак, добавьте тип для этого. Если вы используете javascript, вам это не понадобится.
export interface FormInputProps { name: string label: string }
FormInputProps.ts
Ввод Text
Это самый основной компонент, о котором нужно позаботиться в первую очередь. Ниже представлен изолированный компонент ввода текста, построенный с помощью Material UI.
import React from 'react' import { Controller, useFormContext } from 'react-hook-form' import TextField from '@material-ui/core/TextField' import {FormInputProps} from "./FormInputProps"; export const FormInputText = ({ name, label }: FormInputProps) => { const { control } = useFormContext() return ( <Controller name={name} control={control} render={({ field: { onChange, value }, fieldState: { error }, formState }) => ( <TextField helperText={error ? error.message : null} size='small' error={!!error} onChange={onChange} value={value} fullWidth label={label} variant='outlined' /> )} /> ) }
FormInputText.tsx
В этом компоненте мы используем свойство control для формы react-hook-form. Оно экспортируется из хука useForm() библиотеки.
Мы также продемонстрировали, как отображать ошибки. Для остальных компонентов пропустим это для краткости.
Ввод Radio
Вторым наиболее распространенным компонентом ввода является селектор Radio. Код для интеграции с material-ui выглядит следующим образом.
import React from 'react' import { FormControl, FormControlLabel, FormHelperText, FormLabel, Radio, RadioGroup } from '@material-ui/core' import { Controller, useFormContext } from 'react-hook-form' import {FormInputProps} from "./FormInputProps"; const options = [ { label: 'Radio Option 1', value:'1' }, { label: 'Radio Option 2', value:'2' }, ] export const FormInputRadio: React.FC<FormInputProps> = ({ name, label }) => { const { control, formState: { errors }} = useFormContext() const errorMessage = errors[name] ? errors[name].message : null return ( <FormControl component='fieldset'> <FormLabel component='legend'>{label}</FormLabel> <Controller name={name} control={control} render={({ field: { onChange, value }, fieldState: { error }, formState }) => ( <RadioGroup aria-label='gender' value={value} onChange={onChange}> {options.map((singleItem) => ( <FormControlLabel value={singleItem.value} control={<Radio />} label={singleItem.label} /> ))} </RadioGroup> )} /> <FormHelperText color={'red'}>{errorMessage ? errorMessage : ''}</FormHelperText> </FormControl> ) }
Нам нужно иметь массив options, в который необходимо передать доступные опции для этого компонента.
Если внимательно присмотреться, то будет видно, что эти два компонента в основном схожи по использованию.
Ввод Dropdown
Наш следующий компонент — это выпадающий список (Dropdown). Почти любая форма нуждается в каком-либо виде выпадающего списка. Код для компонента Dropdown выглядит следующим образом
import React from 'react' import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core' import { useFormContext, Controller } from 'react-hook-form' import {FormInputProps} from "./FormInputProps"; const options = [ { label: 'Dropdown Option 1', value:'1' }, { label: 'Dropdown Option 2', value:'2' }, ] export const FormInputDropdown: React.FC<FormInputProps> = ({ name, label }) => { const { control } = useFormContext() const generateSingleOptions = () => { return options.map((option: any) => { return ( <MenuItem key={option.value} value={option.value}> {option.label} </MenuItem> ) }) } return ( <FormControl size={'small'}> <InputLabel>{label}</InputLabel> <Controller render={({ field }) => ( <Select id='site-select' inputProps={{ autoFocus: true }} {...field}> {generateSingleOptions()} </Select> )} control={control} name={name} /> </FormControl> ) }
FormInputDropdown.tsx
В этом компоненте мы убрали ошибку с отображением метки. Он будет таким же, как Radio.
Ввод Date
Это распространенный, но при этом особенный компонент ввода даты. В Material UI у нас нет ни одного компонента Date, который работал бы «из коробки». Для этого нам необходимы вспомогательные библиотеки.
Сначала установите эти зависимости
yarn add @date-io/date-fns@1.3.13 @material-ui/pickers@3.3.10 date-fns@2.22.1
Будьте осторожны с версиями. Это может привести к некоторым странностям. Нам также нужно обернуть наш компонент ввода данных специальной оберткой.
import React from 'react' import DateFnsUtils from '@date-io/date-fns' import {KeyboardDatePicker, MuiPickersUtilsProvider} from '@material-ui/pickers' import { Controller, useFormContext } from 'react-hook-form' import {FormInputProps} from "./FormInputProps"; const DATE_FORMAT = 'dd-MMM-yy' export const FormInputDate = ({ name, label }: FormInputProps) => { const { control } = useFormContext() return ( <MuiPickersUtilsProvider utils={DateFnsUtils}> <Controller name={name} control={control} render={({ field, fieldState, formState }) => ( <KeyboardDatePicker fullWidth variant='inline' defaultValue={new Date()} id={`date-${Math.random()}`} label={label} rifmFormatter={(val) => val.replace(/[^[a-zA-Z0-9-]*$]+/gi, '')} refuse={/[^[a-zA-Z0-9-]*$]+/gi} autoOk KeyboardButtonProps={{ 'aria-label': 'change date' }} format={DATE_FORMAT} {...field} /> )} /> </MuiPickersUtilsProvider> ) }
FormInputDate.tsx
Я выбрал date-fns. Вы можете выбрать другие, например moment.
Ввод Checkbox
Это самый сложный компонент (флажок). Не существует четких примеров использования этого компонента с react-hook-form. Для обработки ввода нам придется немного поработать вручную.
Здесь мы контролируем выбранные состояния, чтобы правильно обрабатывать вводимые данные.
import React, { useEffect, useState } from 'react' import { Checkbox, FormControl, FormControlLabel, FormHelperText, FormLabel } from '@material-ui/core' import { Controller, useFormContext } from 'react-hook-form' import {FormInputProps} from "./FormInputProps"; const options = [ { label: 'Checkbox Option 1', value:'1' }, { label: 'Checkbox Option 2', value:'2' }, ] export const FormInputCheckbox: React.FC<FormInputProps> = ({ name, label }) => { const [selectedItems, setSelectedItems] = useState<any>([]) const { control, setValue, formState: { errors }} = useFormContext() const handleSelect = (value:any) => { const isPresent = selectedItems.indexOf(value) if (isPresent !== -1) { const remaining = selectedItems.filter((item:any) => item !== value) setSelectedItems(remaining) } else { setSelectedItems((prevItems:any) => [...prevItems, value]) } } useEffect(() => { setValue(name, selectedItems) }, [selectedItems]) const errorMessage = errors[name] ? errors[name].message : null return ( <FormControl size={'small'} variant={'outlined'}> <FormLabel component='legend'>{label}</FormLabel> <div> {options.map((option:any) => { return ( <FormControlLabel control={ <Controller name={name} render={({ field: { onChange: onCheckChange } }) => { return <Checkbox checked={selectedItems.includes(option.value)} onChange={() => handleSelect(option.value)} /> }} control={control} /> } label={option.label} key={option.value} /> ) })} </div> <FormHelperText>{errorMessage ? errorMessage : ''}</FormHelperText> </FormControl> ) }
FormInputCheckbox.tsx
Теперь вы просто даете ему список опций, и все работает как надо!
Ввод Slider
Наш последний компонент — это компонент Slider (слайдер). Он является достаточно распространенным. Код прост для понимания
import React, {ChangeEvent, useEffect} from 'react' import { FormLabel, Slider} from '@material-ui/core' import { Controller, useFormContext } from 'react-hook-form' import {FormInputProps} from "./FormInputProps"; export const FormInputSlider = ({ name, label }: FormInputProps) => { const { control , watch} = useFormContext() const [value, setValue] = React.useState<number>(30); const formValue = watch(name) useEffect(() => { if (value) setValue(formValue) }, [formValue]) const handleChange = (event: any, newValue: number | number[]) => { setValue(newValue as number); }; return ( <> <FormLabel component='legend'>{label}</FormLabel> <Controller name={name} control={control} render={({ field, fieldState, formState }) => ( <Slider {...field} value={value} onChange={handleChange} valueLabelDisplay='auto' min={0} max={100} step={1} /> )} /> </> ) }
Вы можете настроить функцию handleChange, чтобы сделать компонент двухсторонним слайдером (полезно для временного диапазона). Просто замените number на number[].
Соедините все вместе
Теперь давайте используем все эти компоненты внутри нашей конечной формы. Это позволит использовать преимущества компонентов для многократного использования, которые мы только что создали.
import {Button, Paper, Typography} from "@material-ui/core"; import { FormProvider, useForm } from 'react-hook-form' import {FormInputText} from "./form-components/FormInputText"; import {FormInputCheckbox} from "./form-components/FormInputCheckbox"; import {FormInputDropdown} from "./form-components/FormInputDropdown"; import {FormInputDate} from "./form-components/FormInputDate"; import {FormInputSlider} from "./form-components/FormInputSlider"; import {FormInputRadio} from "./form-components/FormInputRadio"; export const FormDemo = () => { const methods = useForm({defaultValues: defaultValues}) const { handleSubmit, reset } = methods const onSubmit = (data) => console.log(data) return <Paper style={{display:"grid" , gridRowGap:'20px' , padding:"20px"}}> <FormProvider {...methods}> <FormInputText name='textValue' label='Text Input' /> <FormInputRadio name={'radioValue'} label={'Radio Input'}/> <FormInputDropdown name='dropdownValue' label='Dropdown Input' /> <FormInputDate name='dateValue' label='Date Input' /> <FormInputCheckbox name={'checkboxValue'} label={'Checkbox Input'} /> <FormInputSlider name={'sliderValue'} label={'Slider Input'} /> </FormProvider> <Button onClick={handleSubmit(onSubmit)} variant={'contained'} > Submit </Button> <Button onClick={() => reset()} variant={'outlined'}> Reset </Button> </Paper> }
FormDemo.tsx
В итоге наша форма выглядит следующим образом.
React-hooks появились в React с версии 16.8, сегодня они используются уже повсеместно. Всех заинтересованных приглашаем на двухдневный онлайн-интенсив, на котором мы разберемся, как работать с React-hooks, создадим компонент с использованием hooks, а также научимся делать кастомные hooks.Поработаем с react-testing-library и научимся тестировать компоненты и кастомные hooks. Интенсив будет полезен frontend JavaScript разработчикам и начинающим React разработчикам. Регистрация доступна по ссылке.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/653017/
Добавить комментарий