Crearea propriei biblioteci de validare React: Caracteristicile (Partea 2)

Publicat: 2022-03-10
Rezumat rapid ↬ În articolul anterior al lui Kristofer, el a explicat cum pot fi implementate părțile de bază ale unei biblioteci de validare. În timp ce următoarea parte se va concentra pe îmbunătățirea experienței dezvoltatorului, articolul de astăzi se va concentra pe adăugarea mai multor funcții la ceea ce a fost creat în partea 1.

Implementarea unei biblioteci de validare nu este chiar atât de dificilă. Nici adăugarea tuturor acestor caracteristici suplimentare care vă fac biblioteca de validare mult mai bună decât restul.

Acest articol va continua implementarea bibliotecii de validare pe care am început să o implementăm în partea anterioară a acestei serii de articole. Acestea sunt caracteristicile care ne vor duce de la o simplă dovadă a conceptului la o bibliotecă reală utilizabilă!

  • Partea 1: Bazele
  • Partea 2: Caracteristicile
  • Partea 3: Experiența

Afișați doar validarea la trimitere

Deoarece validăm toate evenimentele de modificare, afișăm mesajele de eroare ale utilizatorului mult prea devreme pentru o experiență bună pentru utilizator. Există câteva moduri prin care putem atenua acest lucru.

Prima soluție este pur și simplu furnizarea steagului submitted ca o proprietate returnată a cârligului useValidation . În acest fel, putem verifica dacă formularul este trimis sau nu înainte de a afișa un mesaj de eroare. Dezavantajul aici este că „afișați codul de eroare” devine puțin mai lung:

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

O altă abordare este de a furniza un al doilea set de erori (să le numim submittedErrors ), care este un obiect gol dacă este submitted este fals și obiectul errors dacă este adevărat. O putem implementa astfel:

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

În acest fel, putem pur și simplu să destructuram tipul de erori pe care vrem să le arătăm. Am putea, bineînțeles, să facem acest lucru și la site-ul de apel - dar furnizând-o aici, îl implementăm o singură dată în loc de în interiorul tuturor consumatorilor.

  • Vedeți demonstrația CodeSandbox care arată cum pot fi utilizate submittedErrors .
Mai multe după săritură! Continuați să citiți mai jos ↓

Afișați mesajele de eroare On-Blur

Mulți oameni doresc să li se afișeze o eroare odată ce părăsesc un anumit câmp. Putem adăuga suport pentru acest lucru, urmărind ce câmpuri au fost „blurred” (navigate departe de) și returnând un obiect blurredErrors , similar cu submittedErrors de mai sus.

Implementarea ne cere să gestionăm un nou tip de acțiune - blur , care va actualiza un nou obiect de stare numit 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'); } }

Când trimitem acțiunea de blur , creăm o nouă proprietate în obiectul de stare blurred cu numele câmpului ca cheie, indicând că acel câmp a fost estompat.

Următorul pas este adăugarea unui element onBlur la funcția noastră getFieldProps , care trimite această acțiune atunci când este cazul:

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

În cele din urmă, trebuie să oferim blurredErrors din cârligul nostru useValidation , astfel încât să putem afișa erorile numai atunci când este necesar.

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

Aici, creăm o funcție memorată care dă seama ce erori trebuie afișate în funcție de faptul că câmpul a fost sau nu neclar. Recalculăm acest set de erori ori de câte ori erorile sau obiectele neclare se schimbă. Puteți citi mai multe despre useMemo hook în documentație.

  • Vedeți demonstrația CodeSandbox

Timpul pentru un mic refactor

Componenta noastră useValidation returnează acum trei seturi de erori - dintre care majoritatea vor arăta la fel la un moment dat. În loc să mergem pe această rută, vom lăsa utilizatorul să specifice în configurație când dorește să apară erorile din formularul său.

Noua noastră opțiune – showErrors – va accepta fie „trimite” (prestabilit), „intotdeauna” sau „blur”. Putem adăuga mai multe opțiuni mai târziu, dacă este nevoie.

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

Deoarece codul de gestionare a erorilor a început să ne ocupe cea mai mare parte a spațiului, îl refactorăm în propria sa funcție. Dacă nu urmați Object.entries și .reduce chestii - este în regulă - este o rescrie a codului for...in din ultima secțiune.

Dacă am solicitat validarea onBlur sau instantanee, am putea specifica elementul showError în obiectul nostru de configurare useValidation .

 const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
  • Vedeți demonstrația CodeSandbox

Notă despre ipoteze

„Rețineți că acum presupun că fiecare formular va dori să afișeze erorile în același mod (întotdeauna pe trimitere, întotdeauna pe blur etc). Acest lucru ar putea fi adevărat pentru majoritatea aplicațiilor, dar probabil nu pentru toate. Să fii conștient de presupunerile tale este o parte importantă a creării API-ului tău.”

Permite validarea încrucișată

O caracteristică cu adevărat puternică a unei biblioteci de validare este de a permite validarea încrucișată - adică de a baza validarea unui câmp pe valoarea altui câmp.

Pentru a permite acest lucru, trebuie să facem cârligul nostru personalizat să accepte o funcție în loc de un obiect. Această funcție va fi apelată cu valorile câmpului curent. Implementarea acestuia este de fapt doar trei linii de cod!

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

Pentru a folosi această caracteristică, putem trece pur și simplu o funcție care returnează obiectul de configurare la 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' } } }));

Aici, folosim valoarea fields.password pentru a ne asigura că două câmpuri de parolă conțin aceeași intrare (ceea ce este o experiență groaznică a utilizatorului, dar asta este pentru o altă postare de blog).

  • Vedeți demo-ul CodeSandbox care nu permite ca numele de utilizator și parola să aibă aceeași valoare.

Adăugați câteva câștiguri de accesibilitate

Un lucru frumos de făcut atunci când sunteți responsabil de elementele de recuzită a unui câmp este să adăugați implicit etichetele aria corecte. Acest lucru va ajuta cititorii de ecran să explice formularul dvs.

O îmbunătățire foarte simplă este să adăugați aria-invalid="true" dacă câmpul are o eroare. Să implementăm asta:

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

Aceasta este o linie de cod adăugată și o experiență de utilizator mult mai bună pentru utilizatorii de cititoare de ecran.

S-ar putea să vă întrebați de ce scriem String(!!state.errors[fieldName]) ? state.errors[fieldName] este un șir, iar operatorul de negație dublă ne oferă o valoare booleană (și nu doar o valoare adevărată sau falsă). Cu toate acestea, proprietatea aria-invalid ar trebui să fie un șir (se poate citi și „gramatică” sau „ortografie”, în plus față de „adevărat” sau „fals”), așa că trebuie să constrângem acel boolean în echivalentul său șir.

Mai sunt încă câteva modificări pe care le-am putea face pentru a îmbunătăți accesibilitatea, dar acesta pare a fi un început corect.

Sintaxa mesajului de validare scurtă

Majoritatea validatorilor din pachetul de calidators (și majoritatea celorlalți validatori, presupun) necesită doar un mesaj de eroare. Nu ar fi frumos dacă am putea pur și simplu să transmitem acel șir în loc de un obiect cu o proprietate de message care conține acel șir?

Să implementăm asta în funcția noastră 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; }

În acest fel, putem rescrie configurația de validare astfel:

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

Mult mai curat!

Valorile inițiale ale câmpului

Uneori, trebuie să validăm un formular care este deja completat. Cârligul nostru personalizat nu acceptă încă asta - așa că să trecem la el!

Valorile inițiale ale câmpului vor fi specificate în configurație pentru fiecare câmp, în proprietatea initialValue . Dacă nu este specificat, este implicit un șir gol.

Vom crea o funcție getInitialState , care va crea starea inițială a reductorului nostru pentru noi.

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

Trecem prin toate câmpurile, verificăm dacă au o proprietate initialValue și setăm valoarea inițială în consecință. Apoi rulăm acele valori inițiale prin validatoare și calculăm și erorile inițiale. Returnăm obiectul de stare inițială, care poate fi apoi transmis cârligului nostru useReducer .

Deoarece introducem un suport non-validator în configurația câmpurilor, trebuie să o omitem atunci când validăm câmpurile. Pentru a face asta, ne schimbăm funcția validateField :

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

Pe măsură ce continuăm să adăugăm mai multe caracteristici ca aceasta, le putem adăuga la matricea noastră specialProps .

  • Vedeți demonstrația CodeSandbox

Rezumând

Suntem pe drumul cel bun pentru a crea o bibliotecă de validare uimitoare. Am adăugat o mulțime de funcții, iar până acum suntem lideri destul de gândiți.

În următoarea parte a acestei serii, vom adăuga toate acele extra care fac ca biblioteca noastră de validare să fie o tendință pe LinkedIn.