Créer votre propre bibliothèque de validation React : les fonctionnalités (partie 2)

Publié: 2022-03-10
Résumé rapide ↬ Dans l'article précédent de Kristofer, il a expliqué comment les éléments de base d'une bibliothèque de validation peuvent être implémentés. Alors que la prochaine partie se concentrera sur l'amélioration de l'expérience des développeurs, l'article d'aujourd'hui se concentrera sur l'ajout de fonctionnalités supplémentaires à ce qui a été créé dans la partie 1.

Implémenter une bibliothèque de validation n'est pas si difficile. Ni l'ajout de toutes ces fonctionnalités supplémentaires qui rendent votre bibliothèque de validation bien meilleure que les autres.

Cet article continuera à implémenter la bibliothèque de validation que nous avons commencé à implémenter dans la partie précédente de cette série d'articles. Ce sont les fonctionnalités qui vont nous faire passer d'une simple preuve de concept à une véritable bibliothèque utilisable !

  • Partie 1 : Les bases
  • Partie 2 : Les fonctionnalités
  • Partie 3 : L'expérience

Afficher uniquement la validation lors de la soumission

Étant donné que nous validons tous les événements de modification, nous affichons les messages d'erreur utilisateur bien trop tôt pour une bonne expérience utilisateur. Il existe plusieurs façons d'atténuer cela.

La première solution consiste simplement à fournir l'indicateur submitted en tant que propriété renvoyée du crochet useValidation . De cette façon, nous pouvons vérifier si le formulaire est soumis ou non avant d'afficher un message d'erreur. L'inconvénient ici est que notre "afficher le code d'erreur" devient un peu plus long :

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

Une autre approche consiste à fournir un deuxième ensemble d'erreurs (appelons-les submittedErrors ), qui est un objet vide si submitted est faux, et l'objet errors si c'est vrai. Nous pouvons l'implémenter comme ceci:

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

De cette façon, nous pouvons simplement déstructurer le type d'erreurs que nous voulons montrer. Bien sûr, nous pourrions également le faire sur le site d'appel, mais en le fournissant ici, nous l'implémentons une seule fois au lieu de l'intégrer à tous les consommateurs.

  • Voir la démo CodeSandbox montrant comment les erreurs submittedErrors peuvent être utilisées.
Plus après saut! Continuez à lire ci-dessous ↓

Afficher les messages d'erreur sur le flou

Beaucoup de gens veulent qu'on leur montre une erreur une fois qu'ils quittent un certain champ. Nous pouvons ajouter un support pour cela, en suivant quels champs ont été "flous" (dont la navigation s'est éloignée) et en renvoyant un objet blurredErrors , similaire aux submittedErrors ci-dessus.

L'implémentation nous oblige à gérer un nouveau type d'action — blur , qui mettra à jour un nouvel objet d'état appelé 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'); } }

Lorsque nous envoyons l'action de blur , nous créons une nouvelle propriété dans l'objet d'état blurred avec le nom du champ comme clé, indiquant que ce champ a été flou.

L'étape suivante consiste à ajouter un accessoire onBlur à notre fonction getFieldProps , qui distribue cette action le cas échéant :

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

Enfin, nous devons fournir les blurredErrors de notre crochet useValidation afin que nous puissions afficher les erreurs uniquement lorsque cela est nécessaire.

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

Ici, nous créons une fonction mémorisée qui détermine les erreurs à afficher selon que le champ a été flouté ou non. Nous recalculons cet ensemble d'erreurs chaque fois que les erreurs ou les objets flous changent. Vous pouvez en savoir plus sur le crochet useMemo dans la documentation.

  • Voir la démo CodeSandbox

Il est temps pour un petit refactor

Notre composant useValidation maintenant trois ensembles d'erreurs, dont la plupart se ressembleront à un moment donné. Au lieu de suivre cette voie, nous allons laisser l'utilisateur spécifier dans la configuration quand il souhaite que les erreurs de son formulaire apparaissent.

Notre nouvelle option - showErrors - acceptera soit "soumettre" (la valeur par défaut), "toujours" ou "flou". Nous pouvons ajouter plus d'options plus tard, si nous en avons besoin.

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

Étant donné que le code de gestion des erreurs a commencé à occuper la majeure partie de notre espace, nous le refactorisons dans sa propre fonction. Si vous ne suivez pas les Object.entries et .reduce - c'est bien - c'est une réécriture du code for...in dans la dernière section.

Si nous avions besoin d'onBlur ou d'une validation instantanée, nous pourrions spécifier la showError dans notre objet de configuration useValidation .

 const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
  • Voir la démo CodeSandbox

Remarque sur les hypothèses

"Notez que je suppose maintenant que chaque formulaire voudra afficher les erreurs de la même manière (toujours sur la soumission, toujours sur le flou, etc.). Cela peut être vrai pour la plupart des applications, mais probablement pas pour toutes. Être conscient de vos hypothèses est une partie importante de la création de votre API. »

Autoriser la validation croisée

Une fonctionnalité vraiment puissante d'une bibliothèque de validation est de permettre la validation croisée, c'est-à-dire de baser la validation d'un champ sur la valeur d'un autre champ.

Pour permettre cela, nous devons faire en sorte que notre crochet personnalisé accepte une fonction au lieu d'un objet. Cette fonction sera appelée avec les valeurs de champ actuelles. Son implémentation ne prend en réalité que trois lignes de code !

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

Pour utiliser cette fonctionnalité, nous pouvons simplement passer une fonction qui renvoie l'objet de configuration à 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' } } }));

Ici, nous utilisons la valeur de fields.password pour nous assurer que deux champs de mot de passe contiennent la même entrée (ce qui est une expérience utilisateur terrible, mais c'est pour un autre article de blog).

  • Voir la démo CodeSandbox qui ne laisse pas le nom d'utilisateur et le mot de passe avoir la même valeur.

Ajoutez des gains d'accessibilité

Une chose intéressante à faire lorsque vous êtes en charge des accessoires d'un champ est d'ajouter les balises aria correctes par défaut. Cela aidera les lecteurs d'écran à expliquer votre formulaire.

Une amélioration très simple consiste à ajouter aria-invalid="true" si le champ contient une erreur. Implémentons cela :

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

C'est une ligne de code supplémentaire et une bien meilleure expérience utilisateur pour les utilisateurs de lecteurs d'écran.

Vous vous demandez peut-être pourquoi nous écrivons String(!!state.errors[fieldName]) ? state.errors[fieldName] est une chaîne, et l'opérateur de double négation nous donne un booléen (et pas seulement une valeur vrai ou faux). Cependant, la propriété aria-invalid doit être une chaîne (elle peut également lire « grammaire » ou « orthographe », en plus de « vrai » ou « faux »), nous devons donc contraindre ce booléen dans son équivalent de chaîne.

Il y a encore quelques ajustements que nous pourrions faire pour améliorer l'accessibilité, mais cela semble être un bon début.

Syntaxe abrégée du message de validation

La plupart des validateurs du package calidators (et la plupart des autres validateurs, je suppose) ne nécessitent qu'un message d'erreur. Ne serait-ce pas bien si nous pouvions simplement passer cette chaîne au lieu d'un objet avec une propriété de message contenant cette chaîne ?

Implémentons cela dans notre fonction 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 cette façon, nous pouvons réécrire notre configuration de validation comme ceci :

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

Beaucoup plus propre !

Valeurs de champ initiales

Parfois, nous devons valider un formulaire déjà rempli. Notre crochet personnalisé ne le prend pas encore en charge — alors allons-y !

Les valeurs de champ initiales seront spécifiées dans la configuration de chaque champ, dans la propriété initialValue . S'il n'est pas spécifié, il s'agit par défaut d'une chaîne vide.

Nous allons créer une fonction getInitialState , qui créera pour nous l'état initial de notre réducteur.

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

Nous parcourons tous les champs, vérifions s'ils ont une propriété initialValue et définissons la valeur initiale en conséquence. Ensuite, nous exécutons ces valeurs initiales dans les validateurs et calculons également les erreurs initiales. Nous renvoyons l'objet d'état initial, qui peut ensuite être passé à notre crochet useReducer .

Puisque nous introduisons un accessoire non validateur dans la configuration des champs, nous devons l'ignorer lorsque nous validons nos champs. Pour ce faire, nous modifions notre fonction validateField :

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

Comme nous continuons à ajouter plus de fonctionnalités comme celle-ci, nous pouvons les ajouter à notre tableau specialProps .

  • Voir la démo CodeSandbox

Résumé

Nous sommes sur la bonne voie pour créer une incroyable bibliothèque de validation. Nous avons ajouté des tonnes de fonctionnalités, et nous sommes maintenant des leaders d'opinion.

Dans la prochaine partie de cette série, nous allons ajouter tous ces extras qui rendent notre bibliothèque de validation même tendance sur LinkedIn.