Tworzenie własnej biblioteki walidacji React: funkcje (część 2)

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ W poprzednim artykule Kristofer wyjaśnił, w jaki sposób można zaimplementować podstawowe części biblioteki walidacyjnej. Podczas gdy następna część skupi się na poprawie doświadczeń programistów, dzisiejszy artykuł skupi się na dodawaniu większej liczby funkcji do tego, co zostało stworzone w Części 1.

Implementacja biblioteki walidacyjnej nie jest wcale taka trudna. Nie jest też dodawanie wszystkich tych dodatkowych funkcji, które sprawiają, że twoja biblioteka walidacji jest znacznie lepsza niż reszta.

Ten artykuł będzie kontynuował wdrażanie biblioteki walidacji, którą zaczęliśmy wdrażać w poprzedniej części tej serii artykułów. Są to cechy, które przeniosą nas od prostego dowodu koncepcji do rzeczywistej, użytecznej biblioteki!

  • Część 1: Podstawy
  • Część 2: Funkcje
  • Część 3: Doświadczenie

Pokaż weryfikację tylko przy przesłaniu

Ponieważ weryfikujemy wszystkie zdarzenia związane ze zmianami, wyświetlamy komunikaty o błędach użytkownika zbyt wcześnie, aby zapewnić użytkownikom dobre wrażenia. Istnieje kilka sposobów na złagodzenie tego.

Pierwszym rozwiązaniem jest po prostu dostarczenie submitted flagi jako zwróconej właściwości haka useValidation . W ten sposób możemy sprawdzić, czy formularz został przesłany przed wyświetleniem komunikatu o błędzie. Minusem jest to, że nasz „pokaż kod błędu” jest nieco dłuższy:

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

Innym podejściem jest dostarczenie drugiego zestawu błędów (nazwijmy je submittedErrors ), który jest pustym obiektem, jeśli submitted ma wartość false, i obiektem errors , jeśli jest prawdziwy. Możemy to zaimplementować tak:

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

W ten sposób możemy po prostu zdestrukturyzować rodzaj błędów, które chcemy pokazać. Oczywiście moglibyśmy to również zrobić w miejscu połączenia — ale udostępniając je tutaj, wdrażamy je raz, a nie u wszystkich konsumentów.

  • Zobacz demo CodeSandbox pokazujące, jak można użyć submittedErrors .
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Pokaż komunikaty o błędach podczas rozmycia

Wiele osób chce zobaczyć błąd po opuszczeniu określonego pola. Możemy dodać obsługę tego, śledząc, które pola zostały „zamazane” (od których odchodzi) i zwracając obiekt blurredErrors , podobnie jak w przypadku błędów submittedErrors powyżej.

Implementacja wymaga od nas obsługi nowego typu akcji — blur , który będzie aktualizował nowy obiekt stanu o nazwie 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'); } }

Kiedy wywołujemy akcję blur , tworzymy nową właściwość w obiekcie stanu blurred z nazwą pola jako kluczem, wskazując, że to pole zostało rozmyte.

Następnym krokiem jest dodanie właściwości onBlur do naszej funkcji getFieldProps , która wywołuje tę akcję, gdy ma to zastosowanie:

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

Na koniec musimy dostarczyć blurredErrors z naszego haka useValidation , abyśmy mogli wyświetlać błędy tylko wtedy, gdy jest to potrzebne.

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

Tutaj tworzymy zapamiętywaną funkcję, która określa, które błędy należy wyświetlić na podstawie tego, czy pole zostało rozmazane. Przeliczamy ten zestaw błędów za każdym razem, gdy zmieniają się błędy lub rozmyte obiekty. Więcej o haczyku useMemo można przeczytać w dokumentacji.

  • Zobacz demo CodeSandbox

Czas na mały refaktor

Nasz komponent useValidation zwraca teraz trzy zestawy błędów — z których większość w pewnym momencie będzie wyglądać tak samo. Zamiast iść tą drogą, pozwolimy użytkownikowi określić w konfiguracji, kiedy chce, aby pojawiły się błędy w jego formularzu.

Nasza nowa opcja — showErrors — zaakceptuje „prześlij” (domyślnie), „zawsze” lub „rozmycie”. Jeśli zajdzie taka potrzeba, możemy dodać więcej opcji później.

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

Ponieważ kod obsługi błędów zaczął zajmować większość naszej przestrzeni, przekształcamy go w jego własną funkcję. Jeśli nie podążasz za Object.entries i .reduce — w porządku — jest to przepisanie kodu for...in z ostatniej sekcji.

Jeśli wymagaliśmy onBlur lub natychmiastowej walidacji, moglibyśmy określić właściwość showError w naszym obiekcie konfiguracyjnym useValidation .

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

Uwaga dotycząca założeń

„Zauważ, że teraz zakładam, że każdy formularz będzie chciał pokazywać błędy w ten sam sposób (zawsze przy przesłaniu, zawsze przy rozmyciu itp.). Może to dotyczyć większości aplikacji, ale prawdopodobnie nie wszystkich. Świadomość swoich założeń jest ogromną częścią tworzenia interfejsu API.”

Zezwól na weryfikację krzyżową

Naprawdę potężną funkcją biblioteki walidacji jest umożliwienie walidacji krzyżowej — to znaczy opieranie walidacji jednego pola na wartości innego pola.

Aby to umożliwić, musimy sprawić, by nasz niestandardowy hook akceptował funkcję zamiast obiektu. Ta funkcja zostanie wywołana z bieżącymi wartościami pól. Implementacja to właściwie tylko trzy linijki kodu!

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

Aby skorzystać z tej funkcji, możemy po prostu przekazać funkcję, która zwraca obiekt konfiguracyjny do 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' } } }));

Tutaj używamy wartości fields.password , aby upewnić się, że dwa pola hasła zawierają te same dane wejściowe (co jest okropnym doświadczeniem użytkownika, ale to na inny wpis na blogu).

  • Zobacz demo CodeSandbox, które nie pozwala, aby nazwa użytkownika i hasło miały tę samą wartość.

Dodaj niektóre wygrane w zakresie ułatwień dostępu

Przyjemną rzeczą do zrobienia, gdy zarządzasz rekwizytami pola, jest domyślne dodanie poprawnych tagów aria. Pomoże to czytnikom ekranu w wyjaśnieniu Twojego formularza.

Bardzo prostym ulepszeniem jest dodanie aria-invalid="true" jeśli pole zawiera błąd. Zaimplementujmy to:

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

To jeden dodatkowy wiersz kodu i znacznie lepsze wrażenia użytkownika dla użytkowników czytników ekranu.

Możesz się zastanawiać, dlaczego piszemy String(!!state.errors[fieldName]) ? state.errors[fieldName] to łańcuch, a operator podwójnej negacji daje nam wartość logiczną (a nie tylko prawdziwą lub fałszywą wartość). Jednak właściwość aria-invalid powinna być ciągiem (może również odczytywać „gramatykę” lub „pisownię” oprócz „prawda” lub „fałsz”), więc musimy przekonwertować tę wartość logiczną na jej odpowiednik w postaci ciągu.

Jest jeszcze kilka poprawek, które moglibyśmy zrobić, aby poprawić dostępność, ale wydaje się to uczciwym początkiem.

Skrócona składnia wiadomości sprawdzania poprawności

Większość walidatorów w pakiecie calidators (i większość innych walidatorów, jak zakładam) wymaga jedynie komunikatu o błędzie. Czy nie byłoby fajnie, gdybyśmy mogli po prostu przekazać ten ciąg zamiast obiektu z właściwością message zawierającego ten ciąg?

Zaimplementujmy to w naszej funkcji 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; }

W ten sposób możemy przepisać naszą konfigurację walidacji w następujący sposób:

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

Dużo czyściej!

Początkowe wartości pól

Czasami musimy zweryfikować już wypełniony formularz. Nasz niestandardowy hak jeszcze tego nie obsługuje — więc przejdźmy do tego!

Początkowe wartości pól zostaną określone w konfiguracji każdego pola we właściwości initialValue . Jeśli nie jest określony, domyślnie jest pustym ciągiem.

Stworzymy funkcję getInitialState , która utworzy dla nas stan początkowy naszego reduktora.

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

Przechodzimy przez wszystkie pola, sprawdzamy, czy mają właściwość initialValue i odpowiednio ustawiamy wartość początkową. Następnie przepuszczamy te wartości początkowe przez walidatory i obliczamy również początkowe błędy. Zwracamy obiekt stanu początkowego, który można następnie przekazać do naszego haka useReducer .

Ponieważ do konfiguracji pól wprowadzamy właściwość niebędącą walidatorem, musimy ją pominąć, gdy sprawdzamy poprawność naszych pól. W tym celu zmieniamy naszą funkcję validateField :

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

W miarę dodawania kolejnych funkcji, takich jak ta, możemy dodać je do naszej tablicy specialProps .

  • Zobacz demo CodeSandbox

Podsumowując

Jesteśmy na dobrej drodze do stworzenia niesamowitej biblioteki walidacji. Dodaliśmy mnóstwo funkcji i już jesteśmy uważanymi za liderów.

W następnej części tej serii dodamy wszystkie te dodatki, które sprawią, że nasza biblioteka walidacji stanie się jeszcze popularniejsza na LinkedIn.