React Hooks помогают упростить концепцию создания динамических форм. Динамическая форма — это форма, в которой пользователь решает, сколько входных данных будет. Новые хуки React, такие как useState, делают создание пользовательского интерфейса проще, чем когда-либо, но создание динамических форм все еще может быть немного сложным для понимания. В этом руководстве объясняется, как создать динамическую форму с помощью React Hooks. Учебник начинается с планирования необходимых компонентов и состояний, затем переходит к рендерингу формы, добавлению динамических входных данных и, наконец, управлению входными данными с помощью событий. В учебнике используется массив объектов cat в состоянии для представления динамических входных данных и кнопка, которая добавляет новые входные данные в массив, вызывая повторную визуализацию формы.

Начало работы с планом

Планирование крайне важно в React. Если вы подумаете о том, чего хотите, до того, как начнете программировать, вы сэкономите много времени. Мы знаем, как это будет выглядеть, но как это будет сделано? Мне нравится проходить небольшой контрольный список React:

  1. Какие компоненты мне понадобятся?
  2. У кого из них будет государство?
  3. Есть ли события, инициированные пользователем?

В этом случае, мне кажется, было бы неплохо иметь основной компонент Form, а затем компонент CatInputs, который обрабатывает динамические входные данные. Что касается состояния, оно понадобится нашему компоненту Form, так как мы будем контролировать каждый ввод. Но нашим CatInputs компонентам не потребуется состояние, так как мы можем просто передать все в качестве реквизита.

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

Порядок атаки

В общем, мне нравится сначала рендерить все, что мне нужно, а затем начинать работать над интерактивностью. В этом случае мы будем визуализировать статическую базу Form, затем выясним, как визуализировать новые входные данные, а затем наконец займемся их управлением. Сначала я создам все в компоненте Form, а затем, когда все заработает, я рефакторинг соответствующего раздела в компонент CatInputs.

Начало работы: рендеринг

Сначала создадим неинтерактивную часть формы (gist link):

// /src/Form.js
import React from 'react'; 
const Form = () => {    
  return (        
    <form>            
      <label htmlFor="owner">Owner</label>   
      <input type="text" name="owner" id="owner" /> 
      <label htmlFor="description">Description</label> 
      <input type="text" name="description" id="description" />
      <input type="button" value="Add New Cat" />            
      <input type="submit" value="Submit" />        
    </form>   
  );
}; export default Form;

Вот что мы сделали (вот такой красивый стиль):

Использование массивов для динамических входных данных

Прежде чем мы начнем программировать, мы должны поговорить о том, как мы собираемся это делать. По сути, у нас будет массив объектов cat в нашем состоянии. Каждый объект будет иметь значение name и age. Наш Form будет перебирать этот список и создавать два новых входа для name и age. Когда мы нажмем кнопку «Добавить новую кошку», мы добавим новый объект в наш массив. Поскольку это изменит наше состояние, это вызовет повторный рендеринг. Затем наша форма будет перебирать этот новый список кошек и добавлять еще пару входных данных.

Для начала давайте просто позаботимся о том, чтобы поместить в наше состояние первый пустой объект-кошку. А вот и новые крючки! Помните, мы используем деструктурирование массива для присваивания, и первый элемент — это само наше состояние, а второй — функция, которую мы используем для его обновления. Мы помещаем наше начальное состояние в качестве параметра useState. Кроме того, вы можете называть их как хотите, я просто поставил состояние в конце, потому что мне это нравится (ссылка на суть):

import React, { useState } from 'react'; 
const Form = () => {  
  const [catState, setCatState] = useState([
    { name: '', age: '' },
  ]);  return (        
    <form>            
      <label htmlFor="owner">Owner</label>   
      <input type="text" name="owner" id="owner" /> 
      <label htmlFor="description">Description</label> 
      <input type="text" name="description" id="description" />
      <input type="button" value="Add New Cat" />      
      {
        catState.map((val, idx) => {
          const catId = `name-${idx}`;
          const ageId = `age-${idx}`;
          return (
            <div key={`cat-${idx}`}>
              <label htmlFor={catId}>{`Cat #${idx + 1}`}</label>
              <input
                type="text"
                name={catId}
                data-idx={idx}
                id={catId}
                className="name" 
              />
              <label htmlFor={ageId}>Age</label>
              <input
                type="text"
                name={ageId}
                data-idx={idx}
                id={ageId}
                className="age"
              />
            </div>
          );      
        })
      }
      <input type="submit" value="Submit" />        
    </form>   
  );
};export default Form;

Это новый большой кусок, но он не сложный, если разбить его на части. Я сопоставляю свой массив кошек из моего catState и использую значение индекса карты, чтобы назначить каждой паре входных данных уникальные идентификаторы, имена, ключи и метки. Вы всегда должны включать метки, чтобы ваш сайт был доступен и удобен для чтения с экрана. И этот атрибут data-idx позже будет иметь решающее значение для управления входными данными. Он сопоставит входные данные с индексом соответствующего объекта-кошки в массиве.

Добавление входов

Итак, мы используем массив, но он еще не динамический. Поскольку наша форма создает два новых ввода, мы знаем, что аспект итерации работает. Но чтобы он был по-настоящему динамичным, мы должны позволить пользователю добавлять входные данные. Нам просто нужно дать нашему компоненту метод, который добавляет новую пустую кошку в наш массив. Нам просто нужно добавить это к нашим входам типа button. Вводы типа button (не элементы кнопки) не отправляют форму, поэтому нам не нужно беспокоиться об остановке отправки (ссылка на суть):

import React, { useState } from 'react'; 
const Form = () => {  
  const blankCat = { name: '', age: '' };
  const [catState, setCatState] = useState([
    {...blankCat}
  ]);
  
  const addCat = () => {
    setCatState([...catState, {...blankCat}]);
  };  return (        
    <form>            
      <label htmlFor="owner">Owner</label>   
      <input type="text" name="owner" id="owner" /> 
      <label htmlFor="description">Description</label> 
      <input type="text" name="description" id="description" />
      <input 
         type="button" 
         value="Add New Cat" 
         onClick={addCat}
      />      
      {
        catState.map((val, idx) => {// rest unchanged

Все, что делает addCat, — это устанавливает состояние с разбросом предыдущего состояния catsarray и новым объектом blankCat, помеченным в конце. Обратите внимание, что я преобразовал начальный объект cat в переменную. Я использую его в качестве основы для клонирования объектов, и если вы не знаете, почему копирование объекта таким образом важно, вот объяснение ссылок на объекты. Также обратите внимание, что нам не нужно что-то вроде prevState для чего-то такого простого. Круто, да? Крючки FUTURE . Теперь всякий раз, когда мы нажимаем нашу кнопку addCat, она добавляет к нашему состоянию одну кошку, которая запускает повторный рендеринг и показывает наш новый ввод, добавленный пользователем!

Управление статическими входами

Теперь, когда мы сделали наши входные данные, давайте проконтролируем их. Сначала легкая часть, нединамические входы. Мы сделаем это, добавив отдельное состояние владельца (gist link):

const Form = () => {
  const [ownerState, setOwnerState] = useState({
    owner: '',
    description: '',
  });
  const handleOwnerChange = (e) => setOwnerState({
    ...ownerState,
    [e.target.name]: [e.target.value],
  });  const blankCat = { name: '', age: '' };
  const [catState, setCatState] = useState([
    { ...blankCat },
  ]);
  
  const addCat = () => {
    setCatState([...catState, { ...blankCat }]);
  };  return (        
    <form>            
      <label htmlFor="owner">Owner</label>   
      <input 
        type="text" 
        name="owner" 
        id="owner" 
        value={ownerState.owner}
        onChange={handleOwnerChange}
      /> 
      <label htmlFor="description">Description</label> 
      <input 
        type="text" 
        name="description" 
        id="description" 
        value={ownerState.owner}
        onChange={handleOwnerChange}     
      />// rest unchanged

Используя другое состояние, мы также делаем вещи более читабельными. Это одно из преимуществ новых крючков состояния, они помогают сделать вещи более цепкими. Чтобы получить значение ввода, которое ввел пользователь, мы используем старый добрый e.target.value. Но мы используем Вычисляемые имена свойств ([] вокруг свойства), чтобы мы могли динамически сопоставлять свойства с помощью атрибута name. Чтобы разбить его, наш ввод owner имеет имя 'owner', что означает, что наше состояние транслируется в owner: "whatever-was-typed". Мы также распространяем текущее состояние владельца. Это очень важно, потому что новый хук useState не объединяет состояние, а полностью его заменяет. Если бы мы не распространялись, то потеряли бы все остальные свойства.

Управление динамическими входами

Теперь о причудливой части; обработка наших динамических входных данных:

const handleCatChange = (e) => {
  const updatedCats = [...catState];
  updatedCats[e.target.dataset.idx][e.target.className] = e.target.value;
  setCatState(updatedCats);
};

Первое, что мы делаем, это клонируем наш catState, чтобы сохранить чистоту рендеринга. Затем мы используем атрибут данных idx, чтобы найти индекс определенного набора входных данных cat. Затем, чтобы узнать, был ли изменен name или age, мы используем атрибут className. Обратите внимание, что мы должны использовать className, а не только name. Это связано с тем, что будет более одного кота, а поскольку атрибут name должен быть уникальным, мы не можем его использовать. Используя className, мы можем просто настроить его так, чтобы он соответствовал именам свойств наших кошек, и на этом покончить.

Все это дает нам точную кошку и свойство, поэтому мы можем использовать e.target.value для фактической установки значения, как и раньше. Наконец, мы вызываем setCatState с нашим обновленным массивом кошек.

Добавление атрибутов value и onChange к входным данным нашего кота имеет небольшой подвох. Хотя функция onChange одинакова, убедитесь, что каждая value получает правильное свойство, либо .name, либо .age:

{
  catState.map((val, idx) => {
    const catId = `name-${idx}`;
    const ageId = `age-${idx}`;
    return (
      <div key={`cat-${idx}`}>
        <label htmlFor={catId}>{`Cat #${idx + 1}`}</label>
        <input
          type="text"
          name={catId}
          data-idx={idx}
          id={catId}
          className="name" 
          value={catState[idx].name}
          onChange={handleCatChange}
        />
        <label htmlFor={ageId}>Age</label>
        <input
          type="text"
          name={ageId}
          data-idx={idx}
          id={ageId}
          className="age"
          value={catState[idx].age}
          onChange={handleCatChange}
        />
      </div>
    );      
  })
}

Прорыв кошачьих входов

Вы только что создали динамическую управляемую форму с помощью React Hooks! Ву! Вот и все в одной части на суть. В качестве последнего шага, почему бы не попробовать вырезать CatInputs как отдельный элемент? Как мы обсуждали ранее, давайте сделаем его чисто функциональным компонентом без состояния, который просто отображает пару входных данных:

import React from 'react';
import PropTypes from 'prop-types';const CatInputs = ({ idx, catState, handleCatChange }) => {
  const catId = `name-${idx}`;
  const ageId = `age-${idx}`;
  return (
      <div key={`cat-${idx}`}>
        <label htmlFor={catId}>{`Cat #${idx + 1}`}</label>
        <input
          type="text"
          name={catId}
          data-idx={idx}
          id={catId}
          className="name" 
          value={catState[idx].name}
          onChange={handleCatChange}
        />
        <label htmlFor={ageId}>Age</label>
        <input
          type="text"
          name={ageId}
          data-idx={idx}
          id={ageId}
          className="age"
          value={catState[idx].age}
          onChange={handleCatChange}
        />
      </div>
    );
};CatInputs.propTypes = {
  idx: PropTypes.number,
  catState: PropTypes.array,
  handleCatChange: PropTypes.func,
};export default CatInputs;

Чтобы увидеть все вместе, ознакомьтесь с последней сутью (и не забудьте использовать реквизиты). Следуйте этому базовому шаблону в качестве отправной точки для вашего следующего проекта и постарайтесь найти некоторые части для оптимизации, как только вы поймете процесс.