Привет друзья!

В данной статье я хочу показать вам, как разработать форму  с динамическими добавлениями полей на React.js с использованием библиотеки react-hook-form и валидацией полей с использованием библиотеки yup на конкретном примере.

Формы являются неотъемлемой частью веб-разработки, и эффективная обработка пользовательского ввода является ключевым аспектом создания интерактивных приложений. Библиотека React Hook Form предоставляет разработчикам мощный инструментарий для упрощения работы с формами в React-приложениях. 

Подготовка и настройка проекта:

Развернем новое приложение на React.js с Typescript с помощью npm:

откройте терминал и запустите следующую команду(убедитесь что у вас установлен npm):

npx create-react-app react_form --template typescript

Затем установим необходимые библиотеки в наше приложения:

откройте терминал в приложении и установите библиотеку  React Hook Form с помощью команды:

npm install react-hook-form

Следующее что нам нужно сделать это установить библиотеку YUP для валидации нашей формы с помощью команды:

 npm i yup

Библиотека YUP является мощным инструментом валидации формы. В этой статье мы ее коснемся базово.

Еще нам понадобится установить специальную обертку(адаптер) для совместной работы YUP и React Hook Form с помощью команды:

npm install @hookform/resolvers/yup

На этом наша подготовка закончена и можно приступать к кодингу.

Для работы с динамическими полями в React Hook Form есть хук useFieldArray, это один из ключевых хуков библиотеки, позволяющий управлять динамическими полями формы. 

Для использования useFieldArray вам понадобится контроллер формы, который вы получаете из useForm хука. Вы должны передать этот контроллер и имя массива полей в useFieldArray с помощью следующих параметров:

  • control: Контроллер формы из useForm.

  • name: Имя массива полей в форме.

Пример использования useFieldArray:

const { control, handleSubmit } = useForm();

const { fields, append, remove, move, update } = useFieldArray({

  control,

  name: 'questions' // имя массива полей в форме

});

 Более подробно о каждом методе:

  • fields представляет собой массив объектов, содержащих значения полей и вспомогательные методы для работы с этим массивом.

  • append - метод для добавления нового элемента в массив полей. Принимает объект с значениями полей в качестве аргумента.

  • remove - метод для удаления элемента из массива полей по индексу.

  • move - метод для перемещения элемента в массиве полей. Принимает два аргумента: текущий индекс элемента и индекс, куда нужно переместить элемент.

  • update - метод для обновления значения поля в массиве полей. Принимает два аргумента: индекс элемента и новое значение.

Разберем работу формы на примере опросника с вложенными полями и динамическими вариантами ответов:

  1. Создадим общую компоненту формы:


import React from 'react';
import {Resolver, useFieldArray, useForm} from 'react-hook-form';
import {yupResolver} from '@hookform/resolvers/yup';
import * as yup from 'yup';
import s from './styles.module.scss';
import {Answers} from "../ansvers/ansvers";
import {Questions} from "../questions/questions";

interface Question {
  questionText: string;
  options: Option[];
}
interface Option {
    optionText: string;
}
export interface FormData {
  questions: Question[];
}
//  Создаем минимальную схему валидации с помощью YUP
const schema = yup.object().shape({
    questions: yup.array().of(
        yup.object().shape({
            questionText: yup.string().required('Введите текст вопроса'),
            options: yup.array().of(
                yup.object().shape({
                    optionText: yup.string().required('Введите текст варианта ответа')
                })
            )
        })
    )
});
export const UseFieldArray = () => {

    // Получаем необходимые методы из хука UseForm
    const { control, handleSubmit } = useForm<FormData>({
        resolver: yupResolver(schema) as Resolver<FormData>
    });

    // Получаем необходимые методы из хука UseFieldArray
    const { fields, append, remove, move  } = useFieldArray<FormData, 'questions'>({
        control,
        name: 'questions'
    });

    // Функция для сабмита формы
    const onSubmit = (data: FormData) => {
        console.log(data);
    };

    // Функция для перемещения варианта ответа вверх в массиве
    const moveUp = (index: number) => {
        if (index > 0) {
            move(index, index - 1)
        }
    };

    // Функция для перемещения варианта ответа вниз в массиве
    const moveDown = (index: number) => {
        if (index < fields.length - 1) {
            move(index, index + 1)
        }
    };

    // Функция для удаления вопроса
    const removeQuestion = (index: number) => {
        remove(index)
    }

    // Функция для добавления нового вопроса
    const addQuestion = () => {
        append({questionText: '', options: []})
    }

    return (
        <form className={s.wrapper_form} onSubmit={handleSubmit(onSubmit)}>
            {fields.map((fieldT, index) => (
                <div className={s.content_form} key={fieldT.id}>
                    <div className={s.question_container}>
                        <label htmlFor={`questions[${index}].questionText`}>Вопрос {index + 1}</label>
                        <div className={s.question}>
                            <Questions questionFieldIndex={index} control={control}/>
                            <button className={s.button} type="button" onClick={() => moveUp(index)}>Вверх</button>
                            <button className={s.button} type="button" onClick={() => moveDown(index)}>Вниз</button>
                            <button className={s.button} type="button" onClick={() => removeQuestion(index)}>Удалить
                                вопрос
                            </button>
                        </div>
                    </div>
                    <Answers
                        control={control}
                        parentFieldIndex={index}
                    />
                    </div>
            ))}
            <div className={s.button_container}>
                <button className={s.button} type="button"
                        onClick={addQuestion}>Добавить вопрос
                </button>
                <button className={s.button} type="submit">Отправить</button>
            </div>
        </form>
);
}

немного нашей форме придадим красоты, что бы не так сильно резала глаз:


.wrapper_form {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
  width: 700px;
  margin: 0 auto;
  gap: 10px;
  padding: 10px;
}
.content_form {
  margin: 15px;
  border-radius: 5px;
  padding: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.question_container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.question {
  display: flex;
  width: 100%;
  justify-content: space-between;
  gap: 10px;
}

.input {
  width: 60%;
  border-radius: 5px;
}
.answers {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.answer {
  display: flex;
  justify-content: space-between;
  margin: 10px;
}

.answer_input {
  width: 80%;
  border-radius: 5px;
}
.button_container {
  display: flex;
  justify-content: space-between;
  margin: 5px;
  gap: 10px;
}

.button {
  background-color: #05A552;
  padding: 10px;
  border: none;
  border-radius: 5px;
  color: white;
}
  1. Вынесем в отдельную компоненту  инпут с вопросом для большей наглядности:

import React from 'react';
import {Control, Controller} from 'react-hook-form';
import s from './styles.module.scss';
import {FormData} from "../useFieldArray/useFieldArray";

interface IProps {
    control: Control<FormData>;
    questionFieldIndex: number;
}
export const 
= ({
                              control,
                              questionFieldIndex
                          }: IProps) => {

    return ( <Controller
                control={control}
            // Обратите внимание для корректной работы нам необходим индекс текущего элемента
                name={`questions.${questionFieldIndex}.questionText`}
                render={({field}) => (
                <input className={s.input} {...field} />
           )}
          />
    )
}

здесь мы передали управление формой в другую компоненту через контроллер, таким образом мы можем большие формы делить на более мелкие, что улучшит читаемость кода и даст возможность переиспользовать эту компоненту в других местах

стили для компоненты Questions:

.wrapper_form {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
  width: 700px;
  margin: 0 auto;
  gap: 10px;
  padding: 10px;
}
.content_form {
  margin: 15px;
  border-radius: 5px;
  padding: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.question_container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.question {
  display: flex;
  width: 100%;
  justify-content: space-between;
  gap: 10px;
}

.input {
  width: 60%;
  border-radius: 5px;
}
.answers {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.answer {
  display: flex;
  justify-content: space-between;
  margin: 10px;
}

.answer_input {
  width: 80%;
  border-radius: 5px;
}
.button_container {
  display: flex;
  justify-content: space-between;
  margin: 5px;
  gap: 10px;
}

.button {
  background-color: #05A552;
  padding: 10px;
  border: none;
  border-radius: 5px;
  color: white;
}
  1. Так же вынесем в отдельную компоненту массив вариантов ответов и тоже установим контроль для массива ответов с помощью useFieldArray:

import React from 'react';
import {Control, Controller, useFieldArray} from 'react-hook-form';
import s from './styles.module.scss';
import {FormData} from '../useFieldArray/useFieldArray'

interface IProps {
    control: Control<FormData>;
    parentFieldIndex: number;
}
export const Answers = ({
                              control,
                              parentFieldIndex
                          }: IProps) => {
    const { fields, append, remove } = useFieldArray({
        control,
        name: `questions.${parentFieldIndex}.options`
    });

    const addAnsver = () => {
        append({
            optionText: ""
        })
    }
    const remoutAnsver = (index: number) => {
        remove(index)
    }

    return (
          <div>
            {fields.map((field, index) => (
                    <div className={s.answers} key={field.id}>
                        <label htmlFor={`questions[${index}].options[${index}]`}>Вариант
                            ответа {index + 1}</label>
                        <div className={s.answer}>
                            <Controller
                                control={control}
                                name={`questions.${parentFieldIndex}.options.${index}.optionText`}
                                render={({field}) => (
                                    <input className={s.input} {...field} />
                                )}
                            />
                            <button className={s.button} type="button"
                                    onClick={() => remoutAnsver(index)}>Удалить
                            </button>
                        </div>
                    </div>
                         )
            )}
            <div className={s.button_container}>
                <button className={s.button} type="button" onClick={addAnsver}>
                    Добавить вариант ответа
                </button>
            </div>
            </div>
    );
}

стили для компоненты Answers:

.input {
  width: 60%;
  border-radius: 5px;
}
.answers {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.answer {
  display: flex;
  justify-content: space-between;
  margin: 10px;
}

.answer_input {
  width: 80%;
  border-radius: 5px;
}
.button_container {
  display: flex;
  justify-content: space-between;
  margin: 5px;
  gap: 10px;
}

.button {
  background-color: #05A552;
  padding: 10px;
  border: none;
  border-radius: 5px;
  color: white;
}

Первое что необходимо это извлечь нужные методы из хуков useForm и useFieldArray, такие как handleSubmit, control, fields, append и remove.

При нажатии кнопки "Добавить вопрос" мы используем метод append из хука useFieldArray, чтобы добавить новый элемент в конец нашего массива. Он берет новый элемент item и добавляет его в конец массива.

При нажатии кнопки "Вверх" или "Вниз" мы используем метод move для перемещения элементов внутри массива. Он принимает два параметра: from - индекс элемента, который нужно переместить, и to - индекс позиции, на которую нужно переместить элемент.

Если мы нажимаем кнопку "Удалить вопрос", то с помощью метода remove мы удаляем элемент из массива по указанному индексу. Метод remove принимает один параметр - index, который указывает на индекс элемента, который нужно удалить.

Если нам нужно вставить элемент в конкретное место, то метод insert поможет нам. Он позволяет вставить новый элемент item на указанную позицию index в массиве. Все элементы, расположенные после указанной позиции, будут сдвинуты вправо.

Работа с массивом ответов с помощью useFieldArray точно такая же как и с основным массивом, только с 1 дополнением нам необходимо знать индекс нашего объекта в основном массиве для корректной работы с подмассивом

 
После вызова любого из этих методов, React автоматически обновляет компонент, чтобы отразить перемещение элемента в пользовательском интерфейсе.

Заключение:


В этой статье мы рассмотрели, как использовать библиотеку react-hook-form и хук useFieldArray для работы с формами, в которых можно динамически добавлять поля. Мы создали пример опросника с вложенными полями вопросов и вариантами ответов, а также показали, как добавлять, удалять и перемещать элементы в массиве.  React Hook Form предоставляет удобные инструменты для работы с формами в приложениях React, позволяя управлять динамическими полями и обрабатывать пользовательский ввод с помощью простого и эффективного API. Использование хука useFieldArray упрощает управление массивами полей формы, делая процесс добавления, удаления и перемещения полей более легким и интуитивно понятным.


Я надеемся, что данная статья помогла вам лучше понять, как использовать react-hook-form и хук useFieldArray для работы массивами в форме в приложениях React.

Ссылка на github

Комментарии (2)


  1. 19Zb84
    24.08.2024 07:44

    import s from './styles.module.scss';

    А почему в коде используется sass а не css модули ?


    1. Aleksei8809 Автор
      24.08.2024 07:44

      Наверное привычка)))