Creación de su propia biblioteca de validación de React: las funciones (parte 2)

Publicado: 2022-03-10
Resumen rápido ↬ En el artículo anterior de Kristofer, explicó cómo se pueden implementar las partes básicas de una biblioteca de validación. Mientras que la siguiente parte se centrará en mejorar la experiencia del desarrollador, el artículo de hoy se centrará en agregar más funciones a lo que se creó en la Parte 1.

Implementar una biblioteca de validación no es tan difícil. Tampoco lo es agregar todas esas características adicionales que hacen que su biblioteca de validación sea mucho mejor que el resto.

Este artículo continuará implementando la biblioteca de validación que comenzamos a implementar en la parte anterior de esta serie de artículos. ¡Estas son las características que nos llevarán de una simple prueba de concepto a una biblioteca utilizable real!

  • Parte 1: Los fundamentos
  • Parte 2: Las características
  • Parte 3: La Experiencia

Solo mostrar validación al enviar

Dado que estamos validando todos los eventos de cambio, estamos mostrando los mensajes de error del usuario demasiado pronto para una buena experiencia de usuario. Hay algunas maneras en que podemos mitigar esto.

La primera solución es simplemente proporcionar el indicador submitted como una propiedad devuelta del gancho useValidation . De esta forma, podemos comprobar si el formulario se envía o no antes de mostrar un mensaje de error. La desventaja aquí es que nuestro "código de error de visualización" se vuelve un poco más largo:

 <label> Username <br /> <input {...getFieldProps('username')} /> {submitted && errors.username && ( <div className="error">{errors.username}</div> )} </label>

Otro enfoque es proporcionar un segundo conjunto de errores (llamémoslos submittedErrors ), que es un objeto vacío si el submitted es falso y el objeto de errors si es verdadero. Podemos implementarlo así:

 const useValidation = config => { // as before return { errors: state.errors, submittedErrors: state.submitted ? state.errors : {}, }; }

De esta manera, podemos simplemente desestructurar el tipo de errores que queremos mostrar. Por supuesto, también podríamos hacer esto en el sitio de la llamada, pero al proporcionarlo aquí, lo estamos implementando una vez en lugar de dentro de todos los consumidores.

  • Vea la demostración de CodeSandbox que muestra cómo se pueden usar los errores submittedErrors .
¡Más después del salto! Continúe leyendo a continuación ↓

Mostrar mensajes de error en desenfoque

Muchas personas quieren que se les muestre un error una vez que abandonan un determinado campo. Podemos agregar soporte para esto, rastreando qué campos han sido "difuminados" (alejados de la navegación) y devolviendo un objeto blurredErrors , similar a los errores submittedErrors anteriormente.

La implementación requiere que manejemos un nuevo tipo de acción: blur , que actualizará un nuevo objeto de estado llamado 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'); } }

Cuando despachamos la acción de blur , creamos una nueva propiedad en el objeto de estado blurred con el nombre del campo como clave, lo que indica que ese campo se ha desdibujado.

El siguiente paso es agregar un accesorio onBlur a nuestra función getFieldProps , que envía esta acción cuando corresponde:

 getFieldProps: fieldName => ({ // as before onBlur: () => { dispatch({ type: 'blur', payload: fieldName }); }, }),

Finalmente, debemos proporcionar los blurredErrors de nuestro useValidation para que podamos mostrar los errores solo cuando sea necesario.

 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, };

Aquí, creamos una función memorizada que determina qué errores mostrar en función de si el campo se ha desenfocado o no. Recalculamos este conjunto de errores cada vez que cambian los errores o los objetos borrosos. Puede leer más sobre el gancho useMemo en la documentación.

  • Ver demostración de CodeSandbox

Hora de un pequeño refactor

Nuestro componente useValidation ahora devuelve tres conjuntos de errores, la mayoría de los cuales tendrán el mismo aspecto en algún momento. En lugar de seguir esta ruta, vamos a dejar que el usuario especifique en la configuración cuándo quiere que aparezcan los errores en su formulario.

Nuestra nueva opción, showErrors , aceptará "enviar" (predeterminado), "siempre" o "desenfoque". Podemos agregar más opciones más tarde, si es necesario.

 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 }; };

Dado que el código de manejo de errores comenzó a ocupar la mayor parte de nuestro espacio, lo estamos refactorizando para que tenga su propia función. Si no sigue las entradas de .reduce Object.entries , está bien, es una reescritura del código for...in en la última sección.

Si necesitáramos onBlur o validación instantánea, podríamos especificar la propiedad showError en nuestro objeto de configuración useValidation .

 const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
  • Ver demostración de CodeSandbox

Nota sobre supuestos

“Tenga en cuenta que ahora asumo que cada formulario querrá mostrar los errores de la misma manera (siempre en envío, siempre en desenfoque, etc.). Eso podría ser cierto para la mayoría de las aplicaciones, pero probablemente no para todas. Ser consciente de sus suposiciones es una gran parte de la creación de su API”.

Permitir validación cruzada

Una característica realmente poderosa de una biblioteca de validación es permitir la validación cruzada, es decir, basar la validación de un campo en el valor de otro campo.

Para permitir esto, debemos hacer que nuestro enlace personalizado acepte una función en lugar de un objeto. Esta función se llamará con los valores de campo actuales. ¡Implementarlo en realidad son solo tres líneas de código!

 function useValidation(config) { const [state, dispatch] = useReducer(...); if (typeof config === 'function') { config = config(state.values); } }

Para usar esta función, simplemente podemos pasar una función que devuelve el objeto de configuración a 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' } } }));

Aquí, usamos el valor de fields.password para asegurarnos de que dos campos de contraseña contengan la misma entrada (lo cual es una experiencia de usuario terrible, pero eso es para otra publicación de blog).

  • Vea la demostración de CodeSandbox que no permite que el nombre de usuario y la contraseña tengan el mismo valor.

Agregue algunas ganancias de accesibilidad

Una buena cosa que puede hacer cuando está a cargo de los accesorios de un campo es agregar las etiquetas aria correctas de forma predeterminada. Esto ayudará a los lectores de pantalla a explicar su formulario.

Una mejora muy simple es agregar aria-invalid="true" si el campo tiene un error. Implementemos eso:

 const useValidation = config => { // as before return { // as before getFieldProps: fieldName => ({ // as before 'aria-invalid': String(!!errors[fieldName]), }), } };

Esa es una línea de código adicional y una experiencia de usuario mucho mejor para los usuarios de lectores de pantalla.

Quizás se pregunte por qué escribimos String(!!state.errors[fieldName]) ? state.errors[fieldName] es una cadena, y el operador de doble negación nos da un valor booleano (y no solo un valor verdadero o falso). Sin embargo, la propiedad aria-invalid debe ser una cadena (también puede leer "gramática" u "ortografía", además de "verdadero" o "falso"), por lo que debemos convertir ese valor booleano en su cadena equivalente.

Todavía hay algunos ajustes más que podríamos hacer para mejorar la accesibilidad, pero parece un buen comienzo.

Sintaxis abreviada del mensaje de validación

La mayoría de los validadores en el paquete calidators (y la mayoría de los otros validadores, supongo) solo requieren un mensaje de error. ¿No sería bueno si pudiéramos simplemente pasar esa cadena en lugar de un objeto con una propiedad de message que contenga esa cadena?

Implementemos eso en nuestra función 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; }

De esta manera, podemos reescribir nuestra configuración de validación así:

 const config = { username: { isRequired: 'The username is required', isEmail: 'The username should be a valid email address', }, };

¡Mucho más limpio!

Valores de campo iniciales

A veces, necesitamos validar un formulario que ya está lleno. Nuestro enlace personalizado aún no es compatible con eso, ¡así que vamos a hacerlo!

Los valores de campo iniciales se especificarán en la configuración de cada campo, en la propiedad initialValue . Si no se especifica, el valor predeterminado es una cadena vacía.

Vamos a crear una función getInitialState , que creará el estado inicial de nuestro reductor para nosotros.

 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, }; }

Revisamos todos los campos, verificamos si tienen una propiedad initialValue y establecemos el valor inicial en consecuencia. Luego ejecutamos esos valores iniciales a través de los validadores y también calculamos los errores iniciales. Devolvemos el objeto de estado inicial, que luego se puede pasar a nuestro useReducer .

Dado que estamos introduciendo un accesorio no validador en la configuración de los campos, debemos omitirlo cuando validamos nuestros campos. Para hacer eso, cambiamos nuestra función validateField :

 function validateField(fieldValue = '', fieldConfig) { const specialProps = ['initialValue']; for (let validatorName in fieldConfig) { if (specialProps.includes(validatorName)) { continue; } // as before } }

A medida que agregamos más funciones como esta, podemos agregarlas a nuestra matriz specialProps .

  • Ver demostración de CodeSandbox

Resumiendo

Estamos bien encaminados para crear una biblioteca de validación increíble. Hemos agregado toneladas de funciones y ahora somos líderes de opinión.

En la siguiente parte de esta serie, vamos a agregar todos esos extras que hacen que nuestra biblioteca de validación sea tendencia en LinkedIn.