Создание собственной библиотеки проверки React: особенности (часть 2)
Опубликовано: 2022-03-10Внедрить библиотеку проверки не так уж и сложно. Также не добавляются все те дополнительные функции, которые делают вашу библиотеку проверки намного лучше, чем остальные.
В этой статье будет продолжена реализация библиотеки проверки, которую мы начали реализовывать в предыдущей части этой серии статей. Это функции, которые переведут нас от простого доказательства концепции к реальной полезной библиотеке!
- Часть 1: Основы
- Часть 2: Особенности
- Часть 3: Опыт
Показывать только подтверждение при отправке
Так как мы проверяем все события изменений, мы слишком рано показываем сообщения об ошибках для пользователей. Есть несколько способов смягчить это.
Первое решение заключается в простом предоставлении submitted
флага в качестве возвращаемого свойства хука useValidation
. Таким образом, мы можем проверить, отправлена ли форма, прежде чем показывать сообщение об ошибке. Недостатком здесь является то, что наш «показать код ошибки» становится немного длиннее:
<label> Username <br /> <input {...getFieldProps('username')} /> {submitted && errors.username && ( <div className="error">{errors.username}</div> )} </label>
Другой подход состоит в том, чтобы предоставить второй набор ошибок (назовем их submittedErrors
), который является пустым объектом, если submitted
равно false, и объектом errors
, если оно истинно. Мы можем реализовать это так:
const useValidation = config => { // as before return { errors: state.errors, submittedErrors: state.submitted ? state.errors : {}, }; }
Таким образом, мы можем просто деструктурировать типы ошибок, которые мы хотим показать. Мы могли бы, конечно, сделать это и на сайте вызова, но, предоставив его здесь, мы реализуем его один раз, а не внутри всех потребителей.
- См. демонстрацию CodeSandbox, показывающую, как можно использовать
submittedErrors
.
Показать сообщения об ошибках при размытии
Многие люди хотят, чтобы им показывали ошибку, когда они покидают определенное поле. Мы можем добавить поддержку для этого, отслеживая, какие поля были «размыты» (от которых уходят), и возвращая объект blurredErrors
, аналогичный submittedErrors
выше сообщениям об ошибках.
Реализация требует, чтобы мы обрабатывали новый тип действия — blur
, который будет обновлять новый объект состояния с именем blurred
:
const initialState = { values: {}, errors: {}, blurred: {}, submitted: false, }; function validationReducer(state, action) { switch (action.type) { // as before case 'blur': const blurred = { ...state.blurred, [action.payload]: true }; return { ...state, blurred }; default: throw new Error('Unknown action type'); } }
Когда мы отправляем действие blur
, мы создаем новое свойство в объекте состояния blurred
с именем поля в качестве ключа, указывающим, что это поле было размыто.
Следующим шагом будет добавление onBlur
в нашу функцию getFieldProps
, которая при необходимости отправляет это действие:
getFieldProps: fieldName => ({ // as before onBlur: () => { dispatch({ type: 'blur', payload: fieldName }); }, }),
Наконец, нам нужно предоставить blurredErrors
из нашего хука useValidation
, чтобы мы могли показывать ошибки только тогда, когда это необходимо.
const blurredErrors = useMemo(() => { const returnValue = {}; for (let fieldName in state.errors) { returnValue[fieldName] = state.blurred[fieldName] ? state.errors[fieldName] : null; } return returnValue; }, [state.errors, state.blurred]); return { // as before blurredErrors, };
Здесь мы создаем запоминаемую функцию, которая вычисляет, какие ошибки отображать, в зависимости от того, размыто поле или нет. Мы пересчитываем этот набор ошибок всякий раз, когда меняются ошибки или размытые объекты. Подробнее о useMemo
можно прочитать в документации.
- См. демонстрацию CodeSandbox
Время для крошечного рефакторинга
Наш компонент useValidation
теперь возвращает три набора ошибок, большинство из которых в какой-то момент времени будут выглядеть одинаково. Вместо того, чтобы идти по этому пути, мы позволим пользователю указать в конфигурации, когда он хочет, чтобы ошибки в его форме отображались.
Наша новая опция — showErrors
— будет принимать либо «отправить» (по умолчанию), «всегда», либо «размытие». Мы можем добавить больше опций позже, если нам нужно.
function getErrors(state, config) { if (config.showErrors === 'always') { return state.errors; } if (config.showErrors === 'blur') { return Object.entries(state.blurred) .filter(([, blurred]) => blurred) .reduce((acc, [name]) => ({ ...acc, [name]: state.errors[name] }), {}); } return state.submitted ? state.errors : {}; } const useValidation = config => { // as before const errors = useMemo( () => getErrors(state, config), [state, config] ); return { errors, // as before }; };
Поскольку код обработки ошибок начал занимать большую часть нашего пространства, мы реорганизуем его в отдельную функцию. Если вы не следите за Object.entries
и .reduce
— ничего страшного — это переписывание кода for...in
в последнем разделе.
Если бы нам требовалась onBlur или мгновенная проверка, мы могли бы указать showError
в нашем объекте конфигурации useValidation
.
const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
- См. демонстрацию CodeSandbox
Примечание о предположениях
«Обратите внимание, что теперь я предполагаю, что каждая форма будет отображать ошибки одинаковым образом (всегда при отправке, всегда при размытии и т. д.). Это может быть верно для большинства приложений, но, вероятно, не для всех. Осознание своих предположений — огромная часть создания вашего API».
Разрешить перекрестную проверку
По-настоящему мощной функцией библиотеки проверки является возможность перекрестной проверки, то есть проверка одного поля на основе значения другого поля.
Чтобы разрешить это, нам нужно заставить наш пользовательский хук принимать функцию вместо объекта. Эта функция будет вызываться с текущими значениями полей. Его реализация на самом деле состоит всего из трех строк кода!
function useValidation(config) { const [state, dispatch] = useReducer(...); if (typeof config === 'function') { config = config(state.values); } }
Чтобы использовать эту функцию, мы можем просто передать функцию, которая возвращает объект конфигурации, в useValidation
:
const { getFieldProps } = useValidation(fields => ({ password: { isRequired: { message: 'Please fill out the password' }, }, repeatPassword: { isRequired: { message: 'Please fill out the password one more time' }, isEqual: { value: fields.password, message: 'Your passwords don\'t match' } } }));
Здесь мы используем значение fields.password
, чтобы убедиться, что два поля пароля содержат один и тот же ввод (что ужасно для пользователя, но это для другого поста в блоге).
- См. демонстрацию CodeSandbox, в которой имя пользователя и пароль не совпадают.
Добавьте некоторые преимущества доступности
Когда вы отвечаете за свойства поля, очень удобно добавлять правильные теги aria по умолчанию. Это поможет программам чтения с экрана объяснить вашу форму.
Очень простое улучшение — добавить aria-invalid="true"
, если в поле есть ошибка. Давайте реализуем это:
const useValidation = config => { // as before return { // as before getFieldProps: fieldName => ({ // as before 'aria-invalid': String(!!errors[fieldName]), }), } };
Это одна добавленная строка кода и гораздо лучший пользовательский интерфейс для пользователей программ чтения с экрана.
Вы можете задаться вопросом, почему мы пишем String(!!state.errors[fieldName])
? state.errors[fieldName]
— это строка, и оператор двойного отрицания дает нам логическое значение (а не только истинное или ложное значение). Однако свойство aria-invalid
должно быть строкой (оно также может читаться как «грамматика» или «орфография» в дополнение к «истине» или «ложи»), поэтому нам нужно привести это логическое значение к его строковому эквиваленту.
Есть еще несколько настроек, которые мы могли бы сделать, чтобы улучшить доступность, но это похоже на честное начало.
Синтаксис сокращенного сообщения проверки
Большинству валидаторов в пакете calidators
(и, я полагаю, большинству других валидаторов) требуется только сообщение об ошибке. Было бы неплохо, если бы мы могли просто передать эту строку вместо объекта со свойством message
, содержащим эту строку?
Давайте реализуем это в нашей функции validateField
:
function validateField(fieldValue = '', fieldConfig, allFieldValues) { for (let validatorName in fieldConfig) { let validatorConfig = fieldConfig[validatorName]; if (typeof validatorConfig === 'string') { validatorConfig = { message: validatorConfig }; } const configuredValidator = validators[validatorName](validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }
Таким образом, мы можем переписать нашу конфигурацию проверки следующим образом:
const config = { username: { isRequired: 'The username is required', isEmail: 'The username should be a valid email address', }, };
Гораздо чище!
Начальные значения полей
Иногда нам нужно проверить уже заполненную форму. Наш пользовательский хук еще не поддерживает это — так что давайте приступим!
Начальные значения полей будут указаны в конфиге для каждого поля, в свойстве initialValue
. Если он не указан, по умолчанию используется пустая строка.
Мы собираемся создать функцию getInitialState
, которая создаст для нас начальное состояние нашего редуктора.
function getInitialState(config) { if (typeof config === 'function') { config = config({}); } const initialValues = {}; const initialBlurred = {}; for (let fieldName in config.fields) { initialValues[fieldName] = config.fields[fieldName].initialValue || ''; initialBlurred[fieldName] = false; } const initialErrors = validateFields(initialValues, config.fields); return { values: initialValues, errors: initialErrors, blurred: initialBlurred, submitted: false, }; }
Мы проходим по всем полям, проверяем, есть ли у них свойство initialValue
, и соответствующим образом устанавливаем начальное значение. Затем мы пропускаем эти начальные значения через валидаторы и также вычисляем начальные ошибки. Мы возвращаем объект начального состояния, который затем можно передать в наш хук useReducer
.
Поскольку мы вводим в конфигурацию полей свойство, не являющееся валидатором, нам нужно пропустить его, когда мы проверяем наши поля. Для этого мы изменим нашу функцию validateField
:
function validateField(fieldValue = '', fieldConfig) { const specialProps = ['initialValue']; for (let validatorName in fieldConfig) { if (specialProps.includes(validatorName)) { continue; } // as before } }
Поскольку мы продолжаем добавлять подобные функции, мы можем добавить их в наш массив specialProps
.
- См. демонстрацию CodeSandbox
Подводя итоги
Мы уже на пути к созданию потрясающей библиотеки проверки. Мы добавили множество функций, и к настоящему времени мы в значительной степени являемся лидерами мнений.
В следующей части этой серии мы добавим все те дополнения, которые сделают нашу библиотеку проверки даже популярной в LinkedIn.