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

ПРИМЕЧАНИЕ. В то время, примерно два года назад, казалось, что интеграция AJV с формой реакции-хука была заброшена, поэтому я сам пошел по собственному пути. Начиная с RHF 2.9.0, в резолверы добавлена ​​поддержка AJV. Я продемонстрирую, как я это сделал, несмотря ни на что, поскольку его можно использовать для интеграции других библиотек проверки или даже вашей собственной пользовательской логики проверки. Я также продемонстрирую встроенный преобразователь, созданный командой RHF — в конце концов, они практически идентичны.

AJV — это отличная библиотека проверки JSON, которая широко загружается на npm. Это также один из старейших и лучший, который я смог найти на основе спецификации JSON Schema. Идея; необработанный JSON более гибок для определения схем проверки, чем что-то программное, такое как Joi или Yup. В конце концов, JSON можно хранить несколькими способами, и его легче отделить от кода. Это также клей, который скрепляет наши приложения!

Если вы когда-либо использовали react-hook-form, то вы уже знаете, насколько фантастична эта маленькая библиотека форм. Быстрый, эффективный и простой в использовании. Он ставит все галочки и не мешает вам, а также предотвращает необходимость повторного создания колеса, как я видел во многих проектах с разработчиками React. Создатели также приложили все усилия, чтобы максимально предотвратить перезагрузку компонентов.

Со всем этим шумом покончено, поехали!

Если хотите, скопируйте источник этой статьи здесь.

Я настроил простой проект React с помощью приложения create-react-app с одной базовой формой:

Вот источник form.js. Довольно простой материал, если вы работали с React и react-hook-form:

import React from "react";
import { useForm } from "react-hook-form";
import { validateResolver } from "./lib/validator";

import "./scss/form.scss";

const Form = () => {
    const { 
        register, 
        handleSubmit, 
        formState: { errors } 
    } = useForm({
        context: "contactForm",
        resolver: validateResolver
    });

    const onSubmit = formData => {
        console.log(`You said: ${JSON.stringify(formData, null, 4)}`)
    };

    return (
        <div>
            <form onSubmit={handleSubmit(onSubmit)}>
                <div>
                    <label>
                        Prefix
                        <select {...register("prefix")}>
                            <option value="">Select One</option>
                            <option value="Mr.">Mr.</option>
                            <option value="Ms.">Ms.</option>
                            <option value="Mrs.">Mrs.</option>
                        </select>
                    </label>
                    {errors.prefix && <span className="validation">{errors.prefix.message}</span>}
                </div>
                <div>
                    <label>
                        First Name
                        <input type="text" {...register("firstName")} />
                    </label>
                    {errors.firstName && <span className="validation">{errors.firstName.message}</span>}
                </div>
                <div>
                    <label>
                        Last Name
                        <input type="text" {...register("lastName")} />
                    </label>
                    {errors.lastName && <span className="validation">{errors.lastName.message}</span>}
                </div>
                <div>
                    <label>
                        Email
                        <input type="text" {...register("email")} />
                    </label>
                    {errors.email && <span className="validation">{errors.email.message}</span>}
                </div>
                <div>
                    <label>
                        Title
                        <input type="text" {...register("title")} />
                    </label>
                    {errors.title && <span className="validation">{errors.title.message}</span>}
                </div>
                <div>
                    <button type="submit">Submit</button>
                </div>
            </form>
        </div>
    );
};

export default Form;

Первое, что следует отметить; Я ссылаюсь на функцию под названием validateResolver в импорте, и она используется в хуке useForm вместе со свойством контекста. Здесь происходит вся магия.

Пользовательский модуль валидатора, который содержит validateResolver, выглядит так:

import Ajv from "ajv";
import AjvFormats from "ajv-formats";
import AjvErrors from "ajv-errors";

//schemas
import { contactForm } from "../schema/contact";

//configure AJV
const ajv = new Ajv({
    allErrors: true,
    strict: false,
    $data: true
});
AjvFormats(ajv);
AjvErrors(ajv);

/**
 * Custom validation resolver - passed to react-hook-form
 */
const validateResolver = async (data, context) => {
    //add schema by name
    ajv.removeSchema(context);
    ajv.addSchema(contactForm, context);

    //pull by context prop in useForm()
    let validate = ajv.getSchema(context);

    // let { schema } = validate;

    //run validation and pull errors
    let isValid = await validate(data);
    let { errors } = validate;

    //hack/workaround for bogus, always-present "required" error
    let errCount = errors.filter(err => err.instancePath !== "").length;

    //let data through?
    if (isValid || errCount === 0) {
        return {
            values: data,
            errors: {}
        };
    }

    //reduce errors to format form expects
    let errorsFormatted = errors.reduce((prev, current) => ({
        ...prev,
        [current.instancePath.replace("/", "")]: current
    }), {});

    //return expected format
    return {
        values: {},
        errors: errorsFormatted
    };
};

export {
    validateResolver
};

Я не буду вдаваться в подробности здесь, так как это довольно простая интеграция, которая соответствует документации по пользовательской проверке формы реакции-хука, которую можно найти здесь.

Параметры конфигурации конструктора Ajv можно найти здесь. По сути, я настроил его, чтобы разрешить пользовательские сообщения об ошибках в схемах и несколько других настроек, которые мне нравятся.

Вот схема contactForm, на которую ссылается импорт:

export const contactForm = {
    type: "object",
    properties: {
        prefix: { 
            type: "string", 
            minLength: 2, 
            maxLength: 4,
            errorMessage: "Select a prefix",
        },
        firstName: { 
            type: "string", 
            minLength: 1, 
            maxLength: 50,
            errorMessage: "Between 1 and 50 characters.",
        },
        lastName: { 
            type: "string", 
            minLength: 1, 
            maxLength: 50,
            errorMessage: "Between 1 and 50 characters.",
        },
        title: { 
            type: "string", 
            maxLength: 50, 
            errorMessage: "Less than 50 characters.",
            nullable: true
        },
        email: { 
            type: "string", 
            maxLength: 100, 
            pattern: "^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$",
            errorMessage: "Invalid email address.",
            nullable: true
        }
    },
    required: ["firstName, lastName", "email"],
    additionalProperties: false
};

TL;DR — функция validateResolver добавляет схему в Ajv, выполняет проверку и либо возвращает переданные ей исходные данные, если они действительны, либо возвращает специально отформатированный объект, который содержит ошибку сообщения, которые ожидает react-hook-form.

На данный момент, вероятно, легко представить, как вы можете использовать свою собственную логику проверки или любую другую неподдерживаемую библиотеку проверки — довольно легко!

После того, как вы клонировали проект, запустите npm install и npm start, чтобы запустить его, вы должны увидеть, что проверка работает, если вы нажмете «Отправить», ничего не заполняя:

Стоит отметить, что если вы устанавливаете реквизит minLength для поля, он по существу обязателен, а реквизит required не нужен. Я включил его, чтобы проиллюстрировать, как справиться с проблемным ответом, когда вы используете required внутри validateResolver.

Когда вы проверите это, вы заметите, что фокус и выделение полей выполняются автоматически. Довольно гладко! Как только вы, наконец, получите действительную форму, вы должны увидеть следующий вывод в консоли:

Поскольку поле заголовка является необязательным, для него остается пустая строка. Поскольку у него нет свойства minLength и его нет в массиве required, вам нужно будет проверить, добавив в поле более 50 символов, после чего вы увидите ошибку проверки:

Вот и все! Конечно, есть бесконечные возможности расширения этого в соответствии с вашими потребностями, если вы используете такой пользовательский валидатор. Например, я хотел иметь возможность приводить значения, чего не делает AJV, из коробки, поскольку это не является частью спецификации JSON Schema.

Я не буду расширять этот пост подробностями, но вот беглый взгляд на то, как я справился с необходимостью приведения пустой строки (например, поля заголовка) к нулевому значению, что было более удобно для серверной части, при отправке данных. к API:

Проверка AJV с использованием встроенного преобразователя

Ранее в этом посте я упоминал, что с тех пор, как я попытался интегрировать эти две технологии, команда RHF добавила встроенную интеграцию. Я быстро просмотрел исходный код, и они, по сути, делают то же, что и я, и код в действии выглядит очень похоже.

import React from "react";
import { useForm } from "react-hook-form";

import { contactForm } from "./schema/contact";
import { ajvResolver } from "@hookform/resolvers/ajv";

import "./scss/form.scss";

const Form = () => {
    const { 
        register, 
        handleSubmit, 
        formState: { errors } 
    } = useForm({
        resolver: ajvResolver(contactForm)
    });

    const onSubmit = formData => {
        console.log(`You said: ${JSON.stringify(formData, null, 4)}`)
    };

    return (
        <div>
            <form onSubmit={handleSubmit(onSubmit)}>
                <div>
                    <label>
                        Prefix
                        <select {...register("prefix")}>
                            <option value="">Select One</option>
                            <option value="Mr.">Mr.</option>
                            <option value="Ms.">Ms.</option>
                            <option value="Mrs.">Mrs.</option>
                        </select>
                    </label>
                    {errors.prefix && <span className="validation">{errors.prefix.message}</span>}
                </div>
                <div>
                    <label>
                        First Name
                        <input type="text" {...register("firstName")} />
                    </label>
                    {errors.firstName && <span className="validation">{errors.firstName.message}</span>}
                </div>
                <div>
                    <label>
                        Last Name
                        <input type="text" {...register("lastName")} />
                    </label>
                    {errors.lastName && <span className="validation">{errors.lastName.message}</span>}
                </div>
                <div>
                    <label>
                        Email
                        <input type="text" {...register("email")} />
                    </label>
                    {errors.email && <span className="validation">{errors.email.message}</span>}
                </div>
                <div>
                    <label>
                        Title
                        <input type="text" {...register("title")} />
                    </label>
                    {errors.title && <span className="validation">{errors.title.message}</span>}
                </div>
                <div>
                    <button type="submit">Submit</button>
                </div>
            </form>
        </div>
    );
};

export default Form;

Одно предостережение: мне не удалось проверить это без «фиктивной» ошибки, вызванной наличием свойства required в схеме проверки JSON. После удаления он вел себя так, как ожидалось. Мне это показалось странным, поскольку оно использовалось в примере на странице @hookform/resolvers npm.

Это пока все! Надеюсь, это было полезно и интересно для вас!