Fermer

mai 16, 2019

Les bases (première partie)


Vous êtes-vous déjà demandé comment fonctionnent les bibliothèques de validation? Cet article explique comment créer étape par étape votre propre bibliothèque de validation pour React. La partie suivante ajoutera des fonctionnalités plus avancées et la dernière partie portera sur l'amélioration de l'expérience des développeurs.

J'ai toujours pensé que les bibliothèques de validation de formulaires étaient plutôt chouettes. Je sais que c’est un intérêt de niche, mais nous les utilisons tellement! Au moins dans mon travail, la plupart de ce que je fais consiste à construire des formulaires plus ou moins complexes avec des règles de validation qui dépendent de choix et de chemins antérieurs. Comprendre le fonctionnement d'une bibliothèque de validation de formulaire est primordial.

L'année dernière, j'ai écrit une telle bibliothèque de validation de formulaire. Je l'ai nommée « Calidation », et vous pouvez lire le billet de blog d'introduction ici . C’est une bonne bibliothèque qui offre beaucoup de flexibilité et utilise une approche légèrement différente de celle des autres sur le marché. Cependant, il existe des tonnes d'autres grandes bibliothèques – la mienne a bien fonctionné pour nos exigences .

Aujourd'hui, je vais vous montrer comment écrire votre propre bibliothèque de validation . pour React. Nous allons suivre le processus étape par étape et vous trouverez des exemples de CodeSandbox au fur et à mesure. À la fin de cet article, vous saurez écrire votre propre bibliothèque de validation ou, à tout le moins, mieux comprendre comment d’autres bibliothèques appliquent «la magie de la validation».

  • Partie 1: Notions fondamentales
  • Deuxième partie: Les caractéristiques
  • Troisième partie: L'expérience

Étape 1: Conception de l'API

La première étape de la création d'une bibliothèque consiste à définir son utilisation. Cela jette les bases de nombreux travaux à venir et, à mon avis, il s'agit de la décision la plus importante que vous fassiez dans votre bibliothèque.

Il est important de créer une API facile à utiliser, et pourtant suffisamment flexible pour permettre des améliorations futures et des cas d'utilisation avancés. Nous allons essayer d’atteindre ces deux objectifs.

Nous allons créer un crochet personnalisé qui acceptera un seul objet de configuration. Cela permettra de passer des options futures sans introduire de changements radicaux.

Une note sur les crochets

Les crochets constituent une nouvelle façon d'écrire React. Si vous avez écrit React dans le passé, il est possible que vous ne reconnaissiez pas certains de ces concepts. Dans ce cas, veuillez consulter la documentation officielle . C’est incroyablement bien écrit et vous explique les bases à connaître.

Nous allons appeler notre crochet personnalisé useValidation pour le moment. Son utilisation pourrait ressembler à ceci:

 const config = {
  des champs: {
    Nom d'utilisateur: {
      isRequired: {message: 'Veuillez renseigner un nom d'utilisateur'},
    },
    mot de passe: {
      isRequired: {message: 'Veuillez renseigner un mot de passe'},
      isMinLength: {valeur: 6, message: 'Merci de le rendre plus sécurisé'}
    }
  },
  onSubmit: e => {/ * handle submit * /}
};
const {getFieldProps, getFormProps, errors} = useValidation (config);

L'objet config accepte un accessoire field qui établit les règles de validation pour chaque champ. De plus, il accepte un rappel pour la date d'envoi du formulaire.

L'objet fields contient une clé pour chaque champ à valider. Chaque champ a sa propre configuration, où chaque clé est un nom de validateur et chaque valeur est une propriété de configuration pour ce validateur. Une autre façon d’écrire la même chose serait:

 {
  des champs: {
    nom de domaine: {
      oneValidator: {validatorRule: 'validator value'},
      anotherValidator: {errorMessage: 'quelque chose ne va pas comme il se doit'}
    }
  }
}

Notre crochet useValidation renverra un objet comportant quelques propriétés – getFieldProps getFormProps et erreurs . Les deux premières fonctions sont ce que Kent C. Dodds appelle des "accesseurs" (voir ici pour un excellent article sur ceux-là ), et est utilisé pour obtenir les accessoires appropriés pour une forme donnée. balise de champ ou de formulaire. Les erreurs prop sont un objet contenant des messages d'erreur, spécifiés par champ.

Cet usage ressemblerait à ceci:

 const config = {...}; // comme ci-dessus
const LoginForm = props => {
  const {getFieldProps, getFormProps, errors} = useValidation (config);
  revenir (
    
); };

Bien! Nous avons donc cloué l’API.

Notez que nous avons également créé une implémentation fictive du crochet useValidation . Pour l'instant, nous renvoyons simplement un objet avec les objets et les fonctions dont nous avons besoin pour ne pas altérer notre exemple d'implémentation.

Stockage de l'état de la forme

La première chose à faire est de tout stocker de l'état de la forme dans notre crochet personnalisé. Nous devons nous rappeler les valeurs de chaque champ, les éventuels messages d'erreur et si le formulaire a été soumis ou non. Nous utiliserons le crochet useReducer dans la mesure où il offre la plus grande flexibilité (et moins de passe-passe-passe). Si vous avez déjà utilisé Redux vous verrez des concepts familiers. Sinon, nous vous expliquerons au fur et à mesure! Nous allons commencer par écrire un réducteur, qui est passé au hook useReducer :

 const initialState = {
  valeurs: {},
  les erreurs: {},
  soumis: faux,
};

validation de fonctionRéducteur (état, action) {
  commutateur (action.type) {
    cas 'changement':
      const values ​​= {... state.values, ... action.payload};
      revenir {
        ...Etat,
        valeurs,
      };
    cas 'soumettre':
      return {... état, soumis: true};
    défaut:
      jeter une nouvelle erreur ('Type d'action inconnu');
  }
}

Qu'est-ce qu'un réducteur? ?

Un réducteur est une fonction qui accepte un objet de valeurs et une «action» et retourne une version augmentée de l'objet de valeurs.

Les actions sont des objets JavaScript simples dotés d'une propriété de type . Nous utilisons une instruction switch pour traiter chaque type d'action possible

On appelle souvent «objet de valeurs» l'état state et, dans notre cas, c'est l'état de notre logique de validation.

Notre état se compose de trois éléments de données – valeurs (les valeurs actuelles de nos champs de formulaire), erreurs (l'ensemble actuel des erreurs messages) et un drapeau est soumis pour indiquer si notre formulaire a été soumis au moins une fois.

Pour stocker notre formulaire, nous devons mettre en œuvre quelques parties de notre useValidation crochet. Lorsque nous appelons notre méthode getFieldProps nous devons renvoyer un objet avec la valeur de ce champ, un gestionnaire de modifications pour le moment où il change et un accessoire de nom permettant de déterminer quel champ correspond à quelle fonction.

 validationRéducteur (état, action) {
  // comme ci-dessus
}

const initialState = {/ * comme ci-dessus * /};

const useValidation = config => {
  const [state, dispatch] = useReducer (validationReducer, initialState);
  
  revenir {
    erreurs: state.errors,
    getFormProps: e => {},
    getFieldProps: fieldName => ({
      onChange: e => {
        if (! config.fields [fieldName]) {
          revenir;
        }
        envoi({
          type: 'change',
          charge utile: {[fieldName]: e.target.value}
        });
      },
      nom: nom de champ,
      valeur: state.values ​​[fieldName],
    }),
  };
};

La méthode getFieldProps renvoie désormais les accessoires requis pour chaque champ. Lorsqu'un événement de changement est déclenché, nous nous assurons que ce champ est dans notre configuration de validation, puis nous informons notre réducteur qu'une action de changement a eu lieu. Le réducteur gérera les modifications apportées à l’état de validation.

Validation de notre formulaire 1965

Notre bibliothèque de validation de formulaire a bonne apparence, mais n’a pas grand-chose à faire pour valider nos valeurs de formulaire! Voyons ça réparer. ?

Nous allons valider tous les champs à chaque événement de modification. Cela n’apparaît peut-être pas très efficace, mais dans les applications du monde réel que je rencontre, ce n’est pas vraiment un problème.

Remarque: nous ne disons pas que vous devez afficher chaque erreur à chaque changement. Nous reviendrons sur la manière d'afficher les erreurs uniquement lorsque vous envoyez ou naviguez en dehors d'un champ, plus loin dans cet article.

Comment choisir les fonctions du validateur

Lorsqu'il est question de validateurs, de nombreuses bibliothèques implémentent toutes les méthodes de validation dont vous avez besoin. Vous pouvez également écrire le vôtre si vous le souhaitez. C’est un exercice amusant!

Pour ce projet, nous allons utiliser un ensemble de validateurs que j’ai écrits il ya quelque temps – calidators . Ces validateurs ont les API suivantes:

 function isRequired (config) {
  fonction de retour (valeur) {
    if (valeur === '') {
      return config.message;
    } autre {
      return null;
    }
  };
}

// ou identique, mais saccadé

const isRequired = config => valeur =>
    valeur === ''? config.message: null;

En d'autres termes, chaque validateur accepte un objet de configuration et retourne un validateur entièrement configuré. Lorsque cette fonction est appelée avec une valeur, elle renvoie le message prop si la valeur est invalide, ou null si elle est valide. Vous pouvez voir comment certains de ces validateurs sont implémentés en consultant le code source .

Pour accéder à ces validateurs, installez le package calidators avec npm install calidators .

Valider un seul champ

Vous souvenez-vous de la config que nous transmettons à notre useValidation ? Cela ressemble à ceci:

 {
  des champs: {
    Nom d'utilisateur: {
      isRequired: {message: 'Veuillez renseigner un nom d'utilisateur'},
    },
    mot de passe: {
      isRequired: {message: 'Veuillez renseigner un mot de passe'},
      isMinLength: {valeur: 6, message: 'Merci de le rendre plus sécurisé'}
    }
  },
  // Plus de matériel
}

Pour simplifier notre implémentation, supposons que nous n’avions qu’un seul champ à valider. Nous allons parcourir chaque clé de l’objet de configuration du champ et exécuter les validateurs un à un jusqu’à ce que nous trouvions une erreur ou que nous ayons terminé la validation.

 import * en tant que validateurs à partir de 'calidators';

fonction validateField (fieldValue = '', fieldConfig) {
  for (laissez validatorName dans fieldConfig) {
    const validatorConfig = fieldConfig [validatorName];
    const validator = validators [validatorName];
    const configurValidator = validator (validatorConfig);
    const errorMessage = configureValidator (fieldValue);

    si (message d'erreur) {
      return errorMessage;
    }
  }
  return null;
}

Nous avons écrit ici une fonction validateField qui accepte la valeur à valider et la configuration du validateur pour ce champ. Nous parcourons tous les validateurs, leur transmettons la configuration de ce validateur et l'exécutons. Si nous obtenons un message d'erreur, nous ignorons le reste des validateurs et retournons. Sinon, nous essayons le prochain validateur.

Remarque: Sur les API de validateur

Si vous choisissez différents validateurs avec différentes API (comme le très populaire validator.js ), cette partie de votre code pourrait regarde un peu différent. Par souci de brièveté, cependant, nous allons laisser cette partie un exercice laissé au lecteur

Remarque: On pour… les boucles

Jamais utilisé pour ... dans boucles auparavant? C’est bien, c’était aussi ma première fois! Fondamentalement, il itère sur les clés d'un objet. Vous en saurez plus à ce sujet sur MDN .

Validez tous les champs

Maintenant que nous avons validé un champ, nous devrions pouvoir valider tous les champs sans trop de problèmes.

 fonction validateField (fieldValue = '', fieldConfig) {
  // comme avant
}

fonction validateFields (fieldValues, fieldConfigs) {
  const errors = {};
  for (laissez fieldName dans fieldConfigs) {
    const fieldConfig = fieldConfigs [fieldName];
    const fieldValue = fieldValues ​​[fieldName];

    erreurs [fieldName] = validateField (fieldValue, fieldConfig);
  }
  retourner les erreurs;
}

Nous avons écrit une fonction validateFields qui accepte toutes les valeurs de champ et la configuration complète du champ. Nous parcourons chaque nom de champ dans la configuration et le validons avec son objet et sa valeur de configuration.

Suivant: Dites à notre réducteur

Très bien, nous avons maintenant cette fonction qui valide l'ensemble de nos données. Tirons-le dans le reste de notre code!

Tout d'abord, nous allons ajouter un gestionnaire d'actions validate à notre validationReducer .

 validationReducer (state, action) ) {
  commutateur (action.type) {
    cas 'changement':
      // comme avant
    cas 'soumettre':
      // comme avant
    cas 'valider':
      return {... state, errors: action.payload};
    défaut:
      jeter une nouvelle erreur ('Type d'action inconnu');
  }
}

Chaque fois que nous déclenchons l'action validate nous remplaçons les erreurs de notre état par ce qui a été passé le long de l'action.

Ensuite, nous allons déclencher notre logique de validation à partir d'un . ] useEffect hook:

 const useValidation = config => {
  const [state, dispatch] = useReducer (validationReducer, initialState);

  useEffect (() => {
    const errors = validateFields (state.fields, config.fields);
    dispatch ({type: 'validate', charge utile: erreurs});
  }, [state.fields, config.fields]);
  
  revenir {
    // comme avant
  };
};

Ce useEffect crochet fonctionne chaque fois que notre état.fields ou config.fields change, en plus du premier montage.

Beware Of Bug ?

Il y a un bogue super subtil dans le code ci-dessus. Nous avons précisé que notre useEffect ne devrait être relancé que lorsque les états.fields ou config.fields changent. En fin de compte, «changement» ne signifie pas nécessairement un changement de valeur! useEffect utilise Object.is pour assurer l'égalité entre les objets, qui à son tour utilise l'égalité de référence. En d'autres termes, si vous transmettez un nouvel objet avec le même contenu, il ne sera plus le même (l'objet étant lui-même nouveau).

Les états.fields sont renvoyés à partir de useReducer qui nous garantit cette égalité de référence, mais notre config est spécifiée inline dans notre composant fonction. Cela signifie que l'objet est recréé à chaque rendu, ce qui déclenchera à son tour l'effet useEffect ci-dessus!

Pour résoudre ce problème, nous devons utiliser l'effet use-deep-compare-effect bibliothèque de Kent C. Dodds. Vous l'installez avec npm install use-deep-compare-effect et vous remplacez votre appel useEffect par celui-ci. Cela garantit que nous effectuons un contrôle d'égalité approfondie au lieu d'un contrôle d'égalité de référence.

Votre code va maintenant ressembler à ceci:

 import useDeepCompareEffect from 'use-deep-compare-effect';

const useValidation = config => {
  const [state, dispatch] = useReducer (validationReducer, initialState);

  useDeepCompareEffect (() => {
    const errors = validateFields (state.fields, config.fields);
    dispatch ({type: 'validate', charge utile: erreurs});
  }, [state.fields, config.fields]);
  
  revenir {
    // comme avant
  };
};
Note sur useEffect

Il s'avère que, useEffect est une fonction assez intéressante. Dan Abramov a écrit un très bel et long article sur les subtilités de useEffect si vous êtes intéressé à apprendre tout ce qu'il y a à propos de ce crochet.

Les choses commencent maintenant. pour ressembler à une bibliothèque de validation!

Traitement de la soumission de formulaire

La dernière pièce de notre bibliothèque de base de validation de formulaire traite de ce qui se passe lorsque nous soumettons le formulaire. À l'heure actuelle, la page est rechargée et rien ne se passe. Ce n’est pas optimal. Nous voulons empêcher le comportement par défaut du navigateur en ce qui concerne les formulaires et le gérer nous-mêmes. Nous plaçons cette logique dans la fonction prop getter getFormProps :

 const useValidation = config => {
  const [state, dispatch] = useReducer (validationReducer, initialState);
  // comme avant
  revenir {
    getFormProps: () => ({
      onSubmit: e => {
        e.preventDefault ();
        dispatch ({type: 'submit'});
        if (config.onSubmit) {
          config.onSubmit (état);
        }
      },
    }),
    // comme avant
  };
};

Nous modifions notre fonction getFormProps pour renvoyer une fonction onSubmit qui est déclenchée chaque fois que l'événement DOM submit est déclenché. Nous évitons le comportement par défaut du navigateur, envoyons une action à notre réducteur que nous avons soumis et appelons le rappel fourni onSubmit avec l’état complet – s’il est fourni.

Résumé

Nous sommes là! Nous avons créé une bibliothèque de validation simple, utilisable et plutôt cool. Il reste cependant encore beaucoup de travail à faire avant de pouvoir dominer les interwebs.

Restez à l’écoute pour la deuxième partie de la semaine prochaine!

 Editorial formidable (dm, il)






Source link