Criando sua própria biblioteca de validação do React: os recursos (parte 2)
Publicados: 2022-03-10Implementar uma biblioteca de validação não é tão difícil. Tampouco adicionar todos esses recursos extras que tornam sua biblioteca de validação muito melhor do que o resto.
Este artigo continuará implementando a biblioteca de validação que começamos a implementar na parte anterior desta série de artigos. Esses são os recursos que nos levarão de uma simples prova de conceito a uma biblioteca real utilizável!
- Parte 1: O básico
- Parte 2: Os Recursos
- Parte 3: A Experiência
Mostrar validação apenas ao enviar
Como estamos validando todos os eventos de alteração, estamos mostrando as mensagens de erro do usuário muito cedo para uma boa experiência do usuário. Existem algumas maneiras de mitigar isso.
A primeira solução é simplesmente fornecer o sinalizador submitted
como uma propriedade retornada do gancho useValidation
. Dessa forma, podemos verificar se o formulário foi enviado ou não antes de mostrar uma mensagem de erro. A desvantagem aqui é que nosso “show error code” fica um pouco mais longo:
<label> Username <br /> <input {...getFieldProps('username')} /> {submitted && errors.username && ( <div className="error">{errors.username}</div> )} </label>
Outra abordagem é fornecer um segundo conjunto de erros (vamos chamá-los de submittedErrors
), que é um objeto vazio se submitted
for false, e o objeto errors
se for true. Podemos implementá-lo assim:
const useValidation = config => { // as before return { errors: state.errors, submittedErrors: state.submitted ? state.errors : {}, }; }
Dessa forma, podemos simplesmente desestruturar o tipo de erro que queremos mostrar. Poderíamos, é claro, fazer isso também no local da chamada — mas, ao fornecê-lo aqui, estamos implementando-o uma vez em vez de dentro de todos os consumidores.
- Veja a demonstração do CodeSandbox mostrando como
submittedErrors
pode ser usado.
Mostrar mensagens de erro em desfoque
Muitas pessoas querem ver um erro quando saem de um determinado campo. Podemos adicionar suporte para isso, rastreando quais campos foram “borrados” (navegados para longe) e retornando um objeto blurredErrors
, semelhante ao submittedErrors
acima.
A implementação exige que lidemos com um novo tipo de ação — blur
, que atualizará um novo objeto de estado chamado 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 despachamos a ação blur
, criamos uma nova propriedade no objeto de estado blur com o nome do campo como chave, indicando que aquele campo foi blurred
.
O próximo passo é adicionar uma prop onBlur
à nossa função getFieldProps
, que despacha esta ação quando aplicável:
getFieldProps: fieldName => ({ // as before onBlur: () => { dispatch({ type: 'blur', payload: fieldName }); }, }),
Finalmente, precisamos fornecer o blurredErrors
do nosso hook useValidation
para que possamos mostrar os erros somente quando necessário.
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, };
Aqui, criamos uma função memorizada que descobre quais erros mostrar com base no fato de o campo ter sido desfocado ou não. Recalculamos esse conjunto de erros sempre que os erros ou objetos borrados mudam. Você pode ler mais sobre o gancho useMemo
na documentação.
- Veja a demonstração do CodeSandbox
Tempo para uma pequena refatoração
Nosso componente useValidation
agora está retornando três conjuntos de erros — a maioria dos quais terá a mesma aparência em algum momento. Em vez de seguir esse caminho, vamos permitir que o usuário especifique na configuração quando deseja que os erros em seu formulário apareçam.
Nossa nova opção — showErrors
— aceitará “enviar” (o padrão), “sempre” ou “desfocar”. Podemos adicionar mais opções mais tarde, se necessário.
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 }; };
Como o código de tratamento de erros começou a ocupar a maior parte do nosso espaço, estamos refatorando-o em sua própria função. Se você não seguir as coisas Object.entries
e .reduce
— tudo bem — é uma reescrita do código for...in
na última seção.
Se precisássemos de onBlur ou validação instantânea, poderíamos especificar a propriedade showError
em nosso objeto de configuração useValidation
.
const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
- Veja a demonstração do CodeSandbox
Nota sobre suposições
“Observe que agora estou assumindo que cada formulário vai querer mostrar os erros da mesma maneira (sempre no envio, sempre em desfoque, etc). Isso pode ser verdade para a maioria dos aplicativos, mas provavelmente não para todos. Estar ciente de suas suposições é uma grande parte da criação de sua API.”
Permitir validação cruzada
Um recurso realmente poderoso de uma biblioteca de validação é permitir a validação cruzada — ou seja, basear a validação de um campo no valor de outro campo.
Para permitir isso, precisamos fazer com que nosso gancho personalizado aceite uma função em vez de um objeto. Esta função será chamada com os valores atuais do campo. Implementá-lo é, na verdade, apenas três linhas de código!
function useValidation(config) { const [state, dispatch] = useReducer(...); if (typeof config === 'function') { config = config(state.values); } }
Para usar esse recurso, podemos simplesmente passar uma função que retorna o objeto de configuração para 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' } } }));
Aqui, usamos o valor de fields.password
para garantir que dois campos de senha contenham a mesma entrada (o que é uma experiência de usuário terrível, mas isso é para outra postagem no blog).
- Veja a demonstração do CodeSandbox que não permite que o nome de usuário e a senha tenham o mesmo valor.
Adicione algumas vitórias de acessibilidade
Uma coisa legal a fazer quando você está no comando dos adereços de um campo é adicionar as aria-tags corretas por padrão. Isso ajudará os leitores de tela a explicar seu formulário.
Uma melhoria muito simples é adicionar aria-invalid="true"
se o campo tiver um erro. Vamos implementar isso:
const useValidation = config => { // as before return { // as before getFieldProps: fieldName => ({ // as before 'aria-invalid': String(!!errors[fieldName]), }), } };
Essa é uma linha de código adicional e uma experiência de usuário muito melhor para usuários de leitores de tela.
Você pode se perguntar por que escrevemos String(!!state.errors[fieldName])
? state.errors[fieldName]
é uma string, e o operador de negação dupla nos dá um valor booleano (e não apenas um valor verdadeiro ou falso). No entanto, a propriedade aria-invalid
deve ser uma string (ela também pode ler “gramática” ou “ortografia”, além de “true” ou “false”), então precisamos forçar esse booleano em sua string equivalente.
Ainda há mais alguns ajustes que poderíamos fazer para melhorar a acessibilidade, mas isso parece um começo justo.
Sintaxe da mensagem de validação abreviada
A maioria dos validadores no pacote de calidators
(e a maioria dos outros validadores, suponho) requerem apenas uma mensagem de erro. Não seria legal se pudéssemos passar essa string em vez de um objeto com uma propriedade de message
contendo essa string?
Vamos implementar isso em nossa função 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; }
Dessa forma, podemos reescrever nossa configuração de validação assim:
const config = { username: { isRequired: 'The username is required', isEmail: 'The username should be a valid email address', }, };
Muito mais limpo!
Valores iniciais do campo
Às vezes, precisamos validar um formulário que já está preenchido. Nosso gancho personalizado ainda não suporta isso - então vamos ao que interessa!
Os valores iniciais dos campos serão especificados na configuração de cada campo, na propriedade initialValue
. Se não for especificado, o padrão é uma string vazia.
Vamos criar uma função getInitialState
, que criará o estado inicial do nosso redutor para nós.
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, }; }
Passamos por todos os campos, verificamos se eles têm uma propriedade initialValue
e definimos o valor inicial de acordo. Em seguida, executamos esses valores iniciais pelos validadores e calculamos os erros iniciais também. Retornamos o objeto de estado inicial, que pode ser passado para nosso gancho useReducer
.
Como estamos introduzindo um prop não validador na configuração de campos, precisamos ignorá-lo quando validamos nossos campos. Para fazer isso, alteramos nossa função validateField
:
function validateField(fieldValue = '', fieldConfig) { const specialProps = ['initialValue']; for (let validatorName in fieldConfig) { if (specialProps.includes(validatorName)) { continue; } // as before } }
À medida que continuamos adicionando mais recursos como esse, podemos adicioná-los ao nosso array specialProps
.
- Veja a demonstração do CodeSandbox
Resumindo
Estamos no caminho certo para criar uma biblioteca de validação incrível. Adicionamos toneladas de recursos e já somos líderes de opinião.
Na próxima parte desta série, vamos adicionar todos os extras que tornam nossa biblioteca de validação ainda mais tendência no LinkedIn.