Tworzenie własnej biblioteki walidacji React: funkcje (część 2)
Opublikowany: 2022-03-10Implementacja 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
.
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.