React - создание динамических форм и модальных компонентов

Внимание

Данный материал является частью цикла статей «Создаем приложение на React с использованием Redux». Не забудьте посмотреть другие статьи по этой теме :-)

  1. React - Настройка проекта и подготовка компонентов
  2. React - навигация и маршрутизация
  3. React - использование Redux в приложении React
  4. React – Lazy Loading и компоненты высшего порядка (HOC)
  5. React - обработка исключений
  6. React - создание динамических форм и модальных компонентов
  7. React - валидация формы и обработка POST запроса
  8. React - обработка запроса PUT для редактирования данных
  9. React - обработка запроса на удаление данных

Компоненты модального окна успеха и ошибки

Начнем с создания следующей структуры папок в директории component:

Сначала мы собираемся изменить файл SuccessModal.js:

import React from 'react';
import Aux from '../../../hoc/Auxiliary/Auxiliary';
import { Modal, Button } from 'react-bootstrap';
import '../ModalStyles.css';

const successModal = (props) => {
    return (
        <Aux>
            <Modal show={props.show} backdrop='static'>
                <Modal.Header>
                    {props.modalHeaderText}
                </Modal.Header>
                <Modal.Body>
                    <p>{props.modalBodyText}</p>
                </Modal.Body>
                <Modal.Footer>
                    <Button bsStyle="success" onClick={props.successClick}>{props.okButtonText}</Button>
                </Modal.Footer>
            </Modal>
        </Aux>
    )
}
 
export default successModal;

Этот код довольно прост. Мы используем компоненты react-bootstrap для модального внутри функционального компонента. Через объект props мы отправляем разные параметры нашему модальному окну и одно событие. Это событие закроет модальное окно после того, как мы нажмем кнопку.

Таким же образом изменим файл ErrorModal.js:

import React from 'react';
import { Modal, Button } from 'react-bootstrap';
import Aux from '../../../hoc/Auxiliary/Auxiliary';
import '../ModalStyles.css';

const errorModal = (props) => {
    return (
        <Aux>
            <Modal show={props.show} backdrop='static'>
                <Modal.Header>
                    {props.modalHeaderText}
                </Modal.Header>
                <Modal.Body>
                    <p>{props.modalBodyText}</p>
                </Modal.Body>
                <Modal.Footer>
                    <Button bsStyle="danger" onClick={props.closeModal}>{props.okButtonText}</Button>
                </Modal.Footer>
            </Modal>
        </Aux>
    )
}
 
export default errorModal;

Далее, нам нужно изменить файл ModalStyles.css:

.modal-header {
    margin: 0;
    line-height: 1.42857143;
    font-size: 30px;
    text-align: center;
}

.modal-body p {
    text-align: center;
    margin-top: 10px;
}

@media (min-width: 768px){
    .modal-dialog {
        width: 500px !important;
        margin: 20% auto !important;
    }
}

Теперь у нас есть модальные компоненты, которые мы можем использовать для создания, обновления или любого другого компонента. Мы намеренно создали два модальных компонента, хотя они почти одинаковы, потому что их гораздо легче читать, когда мы используем модальные компоненты внутри родительских компонентов. Вы можете сразу понять, какова цель модального окна, просто взглянув на его имя.

Создание конфигурации для элементов ввода

Внутри папки src мы собираемся создать папку Utility и внутри нее новый файл InputConfiguration.js. В этом файле мы собираемся сохранить все наши настройки конфигурации для элементов ввода (имя, адрес, дата рождения), которые мы собираемся использовать в формах добавления и редактирования.

Мы уже установили библиотеку моментов в предыдущем посте, но если вы еще этого не сделали, сделайте это (она нужна нам для элемента управления datepicker) с помощью следующей команды:

npm install --save moment

Теперь давайте изменим файл InputConfiguration.js:

import moment from 'moment';

export const returnInputConfiguration = () => {
    return {
        name: {
            element: 'input', type: 'text', value: '', 
            validation: { required: true }, valid: false, touched: false,
            errorMessage: '', label: 'Name:'
        },
        address: {
            element: 'input', type: 'text', value: '', 
            validation: { required: true, maxLength: 60 }, valid: false, touched: false,
            errorMessage: '', label: 'Address:'
        },
        dateOfBirth: {
            element: 'datePicker', type: 'text', value: moment(), 
            valid: true, touched: false,
            errorMessage: '', label: 'Date of birth:'
        }
    }
}

Позже, в нашем компоненте create, мы собираемся преобразовать этот объект в массив объектов и отправить его компоненту Input для создания всех полей ввода, которые нам нужны в форме. Этот массив будет состоять из объектов (пар ключ-значение), где ключом будет имя, адрес или дата рождения (свойства из указанного выше объекта), а значение будет полной частью конфигурации того же объекта. (тип, значение, элемент…).

Динамическое создание входных элементов

Первое, что нам нужно сделать, это установить библиотеку react-datepicker, потому что мы собираемся использовать ее для элемента управления датой рождения.

Выполним команду:

npm install react-datepicker@1.4.0 --save

В папке src создадим папку UI. Внутри нее мы собираемся создать новую папку Input и внутри нее файлы Input.js и Input.css:

Файл Input.js будет функциональным компонентом, поэтому давайте изменим его соответствующим образом:

import React from 'react';
import Aux from '../../hoc/Auxiliary/Auxiliary';
import { FormGroup, Col, FormControl, ControlLabel } from 'react-bootstrap';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import './Input.css';

const input = (props) => {
    let inputField = null;
    let errorMessage = null;

    if(props.invalid && props.shouldValidate && props.touched){
        errorMessage = (<em>{props.errorMessage}</em>);
    }
    
    return (
        <Aux>
            {inputField}
        </Aux>
    )
}
 
export default input;

На данный момент мы просто импортируем все необходимые нам ресурсы, инициализируем поле ввода и сообщение об ошибке и установим значение для этого сообщения, если элемент управления недействителен, если он должен быть проверен и если сделать его активным. Мы не хотим отображать ошибку, если пользователь вообще не поместил курсор мыши внутри этого компонента. Далее, мы возвращаем это поле ввода (которое на данный момент имеет значение NULL) родительскому компоненту.

Под оператором if и над блоком return мы собираемся добавить код для заполнения свойства inputField:

switch (props.elementType) {
    case 'input':
        inputField = (
            <FormGroup controlId={props.id}>
                <Col componentClass={ControlLabel} sm={2}>
                    {props.label}
                </Col>
                <Col sm={6}>
                    <FormControl type={props.type} value={props.value} onChange={props.changed} onBlur={props.blur} />
                </Col>
                <Col>
                    <em>{errorMessage}</em>
                </Col>
            </FormGroup>
        )
        break;
    case 'datePicker':
        inputField = (
            <FormGroup controlId={props.id}>
                <Col componentClass={ControlLabel} sm={2}>
                    {props.label}
                </Col>
                <Col sm={6}>
                    <DatePicker selected={props.value} dateFormat="MM/DD/YYYY" readOnly
                        onChange={props.changed} className='datePickerControl' 
                        showYearDropdown dropdownMode="select"/>
                </Col>
                <Col>
                    <em>{errorMessage}</em>
                </Col>
            </FormGroup>
        )
        break;
    default: inputField = null;
}

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

Осталось изменить файл Input.css:

em {
    color: red;
    margin: 5px 0;
}

.react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name {

    display: inline-block;
    width: 30px!important;
    height: 30px;
    line-height: 30px;
    text-align: center;
    margin: 0.166rem;
    font-size: 14px;
}

.react-datepicker__month-container select {
    font-size: 12px!important;
}

.datePickerControl {
    display: block;
    width: 100%;
    height: 34px;
    padding: 6px 12px;
    font-size: 14px;
    line-height: 1.42857143;
    color: #555;
    background-color: #fff;
    background-image: none;
    border: 1px solid #ccc;
    border-radius: 4px;
}

.react-datepicker__input-container {
    position: relative;
    display: inline-block;
    width: 100%;
}
.react-datepicker-wrapper {
    display: inline-block;
    width: 100%;
}

В этом классе мы переопределяем некоторые из собственных классов datePicker и добавляем один настраиваемый класс (.datePickerControl).

Вот и все, теперь мы можем перейти к компоненту CreateOwner.

Компонент CreateOwner

В папке containers, а затем внутри папки Owner создайте новую папку и назовите ее CreateOwner. Внутри создайте новый файл CreateOwner.js.

Приступим к редактированию этого файла:

import React, { Component } from 'react';
import Input from '../../../UI/Inputs/Input';
import { Form, Well, Button, FormGroup, Col } from 'react-bootstrap';
import { returnInputConfiguration } from '../../../Utility/InputConfiguration';

class CreateOwner extends Component {
    state = {
        ownerForm: {},
        isFormValid: false
    }

    componentWillMount = () =>{
        this.setState({ ownerForm: returnInputConfiguration() });
    }
    
    render() { 
        return ( 
            <Well>

            </Well>
         )
    }
}
 
export default CreateOwner;

В приведенном выше коде мы импортируем все необходимые компоненты react-bootstrap и функцию returnInputConfiguration из файла InputConfiguration.js. Этот компонент является компонентом класса или компонентом с отслеживанием состояния, и в хуке жизненного цикла componentWillMount мы обновляем наше локальное состояние с учетом всей конфигурации формы. Хук compnentWillMount сработает непосредственно перед установкой компонента.

Давайте добавим еще одну строку кода между блоками render и return:

const formElementsArray = formUtilityActions.convertStateToArrayOfFormObjects({ ...this.state.ownerForm });

В функции convertStateToArrayOfFormObjects мы хотим преобразовать объект ownerForm в массив объектов, чтобы отправить его компоненту Input. Итак, давайте добавим эту функцию в отдельный файл.

Внутри папки Utility создайте новый файл FormUtility.js. Измените этот файл, добавив функцию преобразования объекта:

export const convertStateToArrayOfFormObjects = (formObject) => {
    const formElementsArray = [];
    for (let key in formObject) {
        formElementsArray.push({
            id: key,
            config: formObject[key]
        });
    }

    return formElementsArray;
}

Теперь нам нужно импортировать эту функцию в файл CreateOwner.js:

import * as formUtilityActions from '../../../Utility/FormUtility';

У нас будет больше действий внутри файла FormUtility.js, поэтому мы импортируем все эти действия в компонент CreateOwner.js.

Мы заполнили formElementsArray, поэтому давайте воспользуемся им для отправки всех свойств компоненту Input.

Объект конфигурации элементов формы

Давайте добавим этот код в тег Well:

<Form horizontal onSubmit={this.createOwner}>
    {
          formElementsArray.map(element => {
                return <Input key={element.id} elementType={element.config.element} 
                id={element.id} label={element.config.label}
                type={element.config.type} value={element.config.value} 
                changed={(event) => this.handleChangeEvent(event, element.id)}
                errorMessage={element.config.errorMessage} 
                invalid={!element.config.valid} shouldValidate={element.config.validation}
                touched={element.config.touched} 
                blur={(event) => this.handleChangeEvent(event, element.id)} />
        })
     }
    <br />
</Form>

В этом коде мы перебираем все объекты (входные конфигурации) внутри formElementsArray и возвращаем компонент Input со всеми необходимыми свойствами и событиями, которые ему требуются. Существует функция handleChangeEvent, и мы собираемся создать эту функцию, чтобы включить проверку и двустороннюю привязку. Но об этом чуть позже.

Чтобы увидеть результат наших текущих действий, давайте изменим файл App.js, добавив другой маршрут к компоненту CreateOwner ниже маршрута OwnerDetails. Не следует забывать и об операторе импорта:

import CreateOwner from './Owner/CreateOwner/CreateOwner';
<Route path="/createOwner" component={CreateOwner} />

Если мы перейдем на страницу CreateOwner, мы увидим такой результат:

Мы создали элементы управления, но пока ничего не можем с ними сделать. Но как только мы реализуем функцию handleChangeEvent, мы сможем изменять и проверять наши входные данные.

Завершение компонента CreateOwner

Давайте добавим кнопки к нашему компоненту, чтобы завершить создание компонента.

Внутри тега Form и ниже тега
добавьте этот код:

<FormGroup>
     <Col mdOffset={6} md={1}>
          <Button type='submit' bsStyle='info' disabled={!this.state.isFormValid}>Create</Button>
     </Col>
     <Col md={1}>
           <Button bsStyle='danger' onClick={this.redirectToOwnerList}>Cancel</Button>
     </Col>
</FormGroup>

Мы добавляем две кнопки, и кнопка «Создать» неактивна, пока форма недействительна.

Теперь наша форма выглядит так:

Все работает как надо.

Заключение

Прочитав этот пост, вы узнали:

  • Как использовать модальные элементы Bootstrap для создания повторно используемых компонентов модального окна.
  • Использовать простой объект конфигурации для создания формы и как передать его в массив объектов.
  • Как передать массив конфигурации с помощью простого кода JSX в представление формы.

Спасибо, что прочитали статью, и я надеюсь, что вы нашли в ней что-то полезное.

В следующей части серии мы узнаем, как проверять наши элементы ввода. Кроме того, мы собираемся использовать CreateComponent для отправки запроса POST на сервер .NET Core Web API.