Creazione della propria libreria di convalida della reazione: le funzionalità (parte 2)
Pubblicato: 2022-03-10L'implementazione di una libreria di convalida non è poi così difficile. Né aggiunge tutte quelle funzionalità extra che rendono la tua libreria di convalida molto migliore del resto.
Questo articolo continuerà a implementare la libreria di convalida che abbiamo iniziato a implementare nella parte precedente di questa serie di articoli. Queste sono le caratteristiche che ci porteranno da un semplice proof of concept a una vera e propria libreria utilizzabile!
- Parte 1: Le basi
- Parte 2: Le caratteristiche
- Parte 3: L'esperienza
Mostra solo convalida all'invio
Poiché stiamo convalidando tutti gli eventi di modifica, stiamo mostrando i messaggi di errore dell'utente troppo presto per una buona esperienza utente. Ci sono alcuni modi in cui possiamo mitigare questo.
La prima soluzione consiste semplicemente nel fornire il flag submitted
come proprietà restituita dell'hook useValidation
. In questo modo, possiamo verificare se il modulo è stato inviato o meno prima di mostrare un messaggio di errore. Lo svantaggio qui è che il nostro "Mostra codice di errore" diventa un po' più lungo:
<label> Username <br /> <input {...getFieldProps('username')} /> {submitted && errors.username && ( <div className="error">{errors.username}</div> )} </label>
Un altro approccio consiste nel fornire una seconda serie di errori (chiamiamola submittedErrors
), che è un oggetto vuoto se submitted
è false e l'oggetto errors
se è vero. Possiamo implementarlo in questo modo:
const useValidation = config => { // as before return { errors: state.errors, submittedErrors: state.submitted ? state.errors : {}, }; }
In questo modo, possiamo semplicemente destrutturare il tipo di errori che vogliamo mostrare. Ovviamente potremmo farlo anche sul sito della chiamata, ma fornendolo qui, lo stiamo implementando una volta invece che all'interno di tutti i consumatori.
- Vedi la demo di CodeSandbox che mostra come è possibile utilizzare
submittedErrors
.
Mostra messaggi di errore su sfocatura
Molte persone vogliono che venga mostrato un errore una volta che lasciano un determinato campo. Possiamo aggiungere il supporto per questo, tracciando quali campi sono stati "sfocati" (navigati lontano da) e restituendo un oggetto blurredErrors
, simile a submittedErrors
sopra.
L'implementazione ci richiede di gestire un nuovo tipo di azione — blur
, che aggiornerà un nuovo oggetto di stato chiamato 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'); } }
Quando inviamo l'azione blur
, creiamo una nuova proprietà nell'oggetto dello stato blurred
con il nome del campo come chiave, indicando che quel campo è stato sfocato.
Il passaggio successivo consiste nell'aggiungere un prop onBlur
alla nostra funzione getFieldProps
, che invia questa azione quando applicabile:
getFieldProps: fieldName => ({ // as before onBlur: () => { dispatch({ type: 'blur', payload: fieldName }); }, }),
Infine, dobbiamo fornire gli blurredErrors
dal nostro hook useValidation
in modo da poter mostrare gli errori solo quando necessario.
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, };
Qui creiamo una funzione memorizzata che determina quali errori mostrare in base al fatto che il campo sia stato sfocato o meno. Ricalcoliamo questo insieme di errori ogni volta che gli errori o gli oggetti sfocati cambiano. Puoi leggere di più useMemo
nella documentazione.
- Vedi la demo di CodeSandbox
Tempo per un piccolo refactor
Il nostro componente useValidation
ora restituisce tre serie di errori, la maggior parte dei quali avrà lo stesso aspetto prima o poi. Invece di seguire questa strada, consentiremo all'utente di specificare nella configurazione quando desidera che gli errori nel modulo vengano visualizzati.
La nostra nuova opzione — showErrors
— accetterà "invia" (l'impostazione predefinita), "sempre" o "sfocatura". Possiamo aggiungere più opzioni in seguito, se necessario.
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 }; };
Poiché il codice di gestione degli errori ha iniziato a occupare la maggior parte del nostro spazio, lo stiamo refactoring nella sua funzione. Se non segui Object.entries
e .reduce
stuff — va bene — è una riscrittura del codice for...in
nell'ultima sezione.
Se abbiamo richiesto onBlur o la convalida istantanea, potremmo specificare il prop showError
nel nostro oggetto di configurazione useValidation
.
const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
- Vedi la demo di CodeSandbox
Nota sulle ipotesi
“Nota che ora presumo che ogni modulo vorrà mostrare gli errori allo stesso modo (sempre all'invio, sempre alla sfocatura, ecc.). Questo potrebbe essere vero per la maggior parte delle applicazioni, ma probabilmente non per tutte. Essere consapevoli dei propri presupposti è una parte enorme della creazione della propria API."
Consenti convalida incrociata
Una funzionalità davvero potente di una libreria di convalida è quella di consentire la convalida incrociata, ovvero basare la convalida di un campo sul valore di un altro campo.
Per consentire ciò, dobbiamo fare in modo che il nostro hook personalizzato accetti una funzione anziché un oggetto. Questa funzione verrà chiamata con i valori di campo correnti. Implementarlo sono in realtà solo tre righe di codice!
function useValidation(config) { const [state, dispatch] = useReducer(...); if (typeof config === 'function') { config = config(state.values); } }
Per utilizzare questa funzione, possiamo semplicemente passare una funzione che restituisce l'oggetto di configurazione 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' } } }));
Qui, utilizziamo il valore di fields.password
per assicurarci che due campi della password contengano lo stesso input (che è un'esperienza utente terribile, ma è per un altro post del blog).
- Vedi la demo di CodeSandbox che non consente a nome utente e password di avere lo stesso valore.
Aggiungi alcune vittorie per l'accessibilità
Una buona cosa da fare quando sei responsabile degli oggetti di scena di un campo è aggiungere i tag aria corretti per impostazione predefinita. Questo aiuterà i lettori dello schermo a spiegare il tuo modulo.
Un miglioramento molto semplice consiste nell'aggiungere aria-invalid="true"
se il campo contiene un errore. Mettiamo in atto che:
const useValidation = config => { // as before return { // as before getFieldProps: fieldName => ({ // as before 'aria-invalid': String(!!errors[fieldName]), }), } };
Questa è una riga di codice aggiunta e un'esperienza utente molto migliore per gli utenti di screen reader.
Potresti chiederti perché scriviamo String(!!state.errors[fieldName])
? state.errors[fieldName]
è una stringa e l'operatore di doppia negazione ci fornisce un valore booleano (e non solo un valore veritiero o falso). Tuttavia, la proprietà aria-invalid
dovrebbe essere una stringa (può anche leggere "grammatica" o "ortografia", oltre a "vero" o "falso"), quindi dobbiamo forzare quel booleano nella sua stringa equivalente.
Ci sono ancora alcune modifiche che potremmo fare per migliorare l'accessibilità, ma questo sembra un buon inizio.
Sintassi del messaggio di convalida abbreviata
La maggior parte dei validatori nel pacchetto calidators
(e la maggior parte degli altri validatori, presumo) richiedono solo un messaggio di errore. Non sarebbe bello se potessimo semplicemente passare quella stringa invece di un oggetto con una proprietà del message
contenente quella stringa?
Implementiamolo nella nostra funzione 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; }
In questo modo, possiamo riscrivere la nostra configurazione di convalida in questo modo:
const config = { username: { isRequired: 'The username is required', isEmail: 'The username should be a valid email address', }, };
Molto più pulito!
Valori di campo iniziali
A volte, abbiamo bisogno di convalidare un modulo che è già compilato. Il nostro hook personalizzato non lo supporta ancora, quindi andiamo al punto!
I valori dei campi iniziali verranno specificati nella configurazione per ogni campo, nella proprietà initialValue
. Se non è specificato, il valore predefinito è una stringa vuota.
Creeremo una funzione getInitialState
, che creerà per noi lo stato iniziale del nostro riduttore.
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, }; }
Esaminiamo tutti i campi, controlliamo se hanno una proprietà initialValue
e impostiamo il valore iniziale di conseguenza. Quindi eseguiamo quei valori iniziali attraverso i validatori e calcoliamo anche gli errori iniziali. Restituiamo l'oggetto dello stato iniziale, che può quindi essere passato al nostro hook useReducer
.
Dal momento che stiamo introducendo un prop non validatore nella configurazione dei campi, dobbiamo saltarlo quando convalidiamo i nostri campi. Per farlo, cambiamo la nostra funzione validateField
:
function validateField(fieldValue = '', fieldConfig) { const specialProps = ['initialValue']; for (let validatorName in fieldConfig) { if (specialProps.includes(validatorName)) { continue; } // as before } }
Man mano che continuiamo ad aggiungere altre funzionalità come questa, possiamo aggiungerle al nostro array specialProps
.
- Vedi la demo di CodeSandbox
Riassumendo
Siamo sulla buona strada per creare una straordinaria libreria di convalida. Abbiamo aggiunto tonnellate di funzionalità e ormai siamo considerati leader.
Nella prossima parte di questa serie, aggiungeremo tutti quegli extra che rendono la nostra libreria di convalida anche di tendenza su LinkedIn.