Эта запись изначально была опубликована в моем блоге

Обработка форм — огромная тема веб-разработки. Есть почти только 2 способа работы с формой, в зависимости от библиотеки/фреймворка, который вы выбрали для использования, и в зависимости от стороны, с которой вы пытаетесь справиться с формой (клиент или сервер):

  • Те, которые включают обработку формы компонента. Этот компонент обычно делает много волшебства и заставляет вас думать о своей форме так, как она будет обрабатываться этим компонентом.
  • Те, которые позволяют вам делать всю работу. А потом вы чувствуете, что это нонсенс — управлять своей формой на таком низком уровне самостоятельно.

Простой вариант использования в React

Пока вы работаете с простой формой в React, все остается довольно простым. В основном вам просто нужно беспокоиться о вашем методе onSubmit, который будет вызывать ваш API для отправки данных формы POST.

Вот базовый пример формы, публикующей сниппет (заголовок и текст) с использованием React, Material UI и Redux (после публикации данных метод addSnippet отправляет событие, чтобы обновить другой компонент )

import React from 'react';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import TextField from 'material-ui/TextField';
import {addSnippet} from "../../../actions/snippets";
import {connect} from "react-redux";
import { bindActionCreators } from 'redux';


class AddSnippet extends React.Component {

    constructor(props) {
        super(props);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleSubmit = (event) => {
        event.preventDefault();
        const form = new FormData(event.target);

        const data = {
            "title": form.get("title"),
            "snippet": form.get("snippet")
        };

        this.props.addSnippet(data);
    };

    render() {
        const actions = [
            <FlatButton
                label="Cancel"
                primary={true}
                onClick={this.handleClose}
                key="cancel"
            />,
            <FlatButton
                type="submit"
                label="Submit"
                primary={true}
                keyboardFocused={true}
                key="submit"
            />,
        ];

        return (
            <div>
                <Dialog
                    title="Adding a snippet"
                    modal={false}
                    open={this.state.open}
                    onRequestClose={this.handleClose}>
                    <form method="POST" onSubmit={this.handleSubmit}>
                        <TextField
                            hintText="My awesome snippet"
                            floatingLabelText="Title"
                            name="title"
                        /><br />
                        <TextField
                            hintText="<?php echo 'Hello World'; ?>"
                            floatingLabelText="Snippet"
                            multiLine={true}
                            rows={5}
                            rowsMax={10}
                            fullWidth={true}
                            name="snippet"
                        /><br />
                        <div style={{ textAlign: 'right', padding: 8, margin: '24px -24px -24px -24px' }}>
                            {actions}
                        </div>
                    </form>
                </Dialog>
            </div>
        );
    }
}

const mapDispatchToProps = dispatch => (bindActionCreators({
    addSnippet
}, dispatch));

export default connect(
    null,
    mapDispatchToProps
)(AddSnippet)

Но потом… вы, вероятно, захотите сделать какую-то проверку.

Первый шаг — выполнить проверку на стороне клиента, например: если поле является обязательным, вы наверняка захотите проверить, не пусто ли поле, прежде чем вызывать API. В этом примере нам пришлось бы делать это вручную. Инициализация состояния с полями, а затем проверка состояния перед вызовом API может быть хорошей идеей. Но все же… Это требует времени и может быть источником ошибок/багов.

Второй шаг — проверка на стороне сервера. Также: вызов вашего API и проверка кода состояния › 299. Затем получите возвращенную полезную нагрузку, скачкообразно, есть общая схема для ошибок проверки и сопоставьте ошибки, отправленные API, с вашими полями. Эта часть немного сложна, а также является источником ошибок/ошибок.

Проверка формы стала проще с Formik

Formik — это реактивная библиотека, которая помогает вам делать основы с формами, не вводя никакой магии. Вы можете рассматривать его как набор инструментов, которые могут вам понадобиться при обработке таких форм, как: handleSubmit, handleChange, handleBlur, setErrors и т. д.

Пример с комментариями вы найдете ниже. Мне потребовалось время, чтобы понять, как правильно обрабатывать ошибки, поступающие от API, поэтому в этом примере также документирован вызов API. Идея состоит в том, чтобы понять все это здесь.

В этом примере используются Formik, Redux, Yup для проверки на стороне клиента и Material UI для дизайна. Форма находится в Dialog (Диалог), который открывается, когда пользователь нажимает на FloatingActionButton (FloatingActionButton)

Имейте в виду, что в случае ошибок API должен возвращать полезную нагрузку, структурированную следующим образом:

{
    "errors": [
        {
            "field": "title",
            "error": "This field is required"
        }
    ]
}

Мой файл компонента: AddSnippet

import React from 'react';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import ContentAdd from 'material-ui/svg-icons/content/add';
import TextField from 'material-ui/TextField';
import {addSnippet} from "../../../actions/snippets";
import {connect} from "react-redux";
import { bindActionCreators } from 'redux';
import { Formik } from 'formik';
import Yup from 'yup'


class AddSnippet extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            open: false,
        };
        this.handleClose = this.handleClose.bind(this);
    }

    handleOpen = () => {
        this.setState({open: true});
    };

    handleClose = () => {
        this.setState({open: false});
    };

    render() {
        return (
            <div>
                <FloatingActionButton secondary={true} onClick={this.handleOpen} style={{
                    margin: 0,
                    top: 'auto',
                    right: 20,
                    bottom: 20,
                    left: 'auto',
                    position: 'fixed',
                }}>
                    <ContentAdd />
                </FloatingActionButton>
                <Dialog
                    title="Adding a snippet"
                    modal={false}
                    open={this.state.open}
                    onRequestClose={this.handleClose}>
                    <Formik
                        initialValues={{title: '', snippet: ''}}
                        onSubmit={async (values, {setFieldError}) => {
                            try {
                                await this.props.addSnippet(values); // Call the api
                                this.handleClose();
                            } catch (errors) { // Catch status code > 299
                                errors.forEach( err => {
                                    setFieldError(err.field, err.error); // Map errors to fields
                                });
                            }
                        }}
                        validationSchema={Yup.object().shape({
                            title: Yup.string() //Client side validation for field "title"
                                .min(3, 'Title must be at least 3 characters long.')
                                .required('Title is required.'),
                            snippet: Yup.string() //Client side validation for field "snippet"
                                .min(3, 'Snippet must be at least 3 characters long.')
                                .required("Snippet is required"),
                        })}
                        component={ this.form }
                    />
                </Dialog>
            </div>
        )
    }

    form = ({handleSubmit, handleChange, handleBlur, values, errors}) => {
        return (
            <form method="POST" onSubmit={handleSubmit}>
                <TextField
                    hintText="My awesome snippet"
                    floatingLabelText="Title"
                    name="title"
                    onChange={handleChange} //By default client side validation is done onChange
                    onBlur={handleBlur} //By default client side validation is also done onBlur
                    value={values.title}
                    errorText={errors.title} //Error display
                /><br />
                <TextField
                    hintText="<?php echo 'Hello World'; ?>"
                    floatingLabelText="Snippet"
                    multiLine={true}
                    rows={5}
                    rowsMax={10}
                    fullWidth={true}
                    name="snippet"
                    onChange={handleChange}
                    onBlur={handleBlur}
                    value={values.snippet}
                    errorText={errors.snippet} //Error display
                /><br />
                <div style={{ textAlign: 'right', padding: 8, margin: '24px -24px -24px -24px' }}>
                    <FlatButton
                        label="Cancel"
                        primary={true}
                        onClick={this.handleClose}
                        key="cancel"
                    />
                    <FlatButton
                        type="submit"
                        label="Submit"
                        primary={true}
                        keyboardFocused={true}
                        key="submit"
                    />
                </div>
            </form>
        );
    }
}

const mapDispatchToProps = dispatch => (bindActionCreators({
    addSnippet
}, dispatch));

//Connect the component to the store. See Redux.
export default connect(
    null,
    mapDispatchToProps
)(AddSnippet)

Файл моих действий (см. Redux):

import {postSnippet} from '../../api/snippets'
import { receiveOneSnippet } from '../'

export function addSnippet(data) {
    return async function (dispatch) {
        try {
            // If  status code == 201 the API return the new created object.
            const response = await postSnippet(data);
            //This object is dispatch to the store in order to update another component
            dispatch(receiveOneSnippet(response))
        } catch (err) {
            //If status code > 299 the payload is catch here.
            throw  err.errors;
        }
    }
}

Где действительно выполняется мой вызов API (в отдельном файле):

export async function postSnippet(data) {
    try {
        const response = await fetch(`mydomain.com/snippets/`, {
            method: 'POST',
            body: JSON.stringify(data),
        });

        //If status code > 299
        if (!response.ok) {
            throw response;
        }
        return await response.json();
    } catch (err) {
        //Throw the return payload
        throw await err.json()
    }
}

Результат :

Первоначально опубликовано на http://le-gall.bzh/post/form-validation-with-formik/.