2 pontos por GN⁺ 2024-10-29 | 1 comentários | Compartilhar no WhatsApp
  • Formulários HTML já contam com recursos nativos de validação como required, type="email", pattern, maxlength e setCustomValidity, mas seu uso real é baixo em relação à amplitude desses recursos
  • O mais poderoso, setCustomValidity, consegue lidar com lógica de validação arbitrária e casos complexos, porém é exposto apenas como método do DOM, o que não combina bem com componentes declarativos
  • Em ambientes como React, para alinhar a validação do valor inicial, useRef, useLayoutEffect e onChange acabam se entrelaçando, e a mesma lógica de validação tende a ser duplicada entre a renderização inicial e o manipulador de mudanças
  • Se existisse uma abstração em forma de atributo como custom-validity, seria possível expressar em um só lugar validações com dependência entre estado e entrada, como verificação assíncrona de nome de usuário duplicado ou confirmação de senha
  • A baixa adoção da validação nativa de formulários parece estar mais ligada a um problema de usabilidade da API do que à falta de recursos, e um API declarativo na especificação HTML ampliaria bastante seu potencial de uso

O que a validação de formulários HTML já oferece

  • A forma mais simples de impedir entradas vazias é adicionar o atributo required
  • Há três maneiras principais de impor restrições à entrada
    • usar tipos de entrada como type="email", type="number" e type="url"
    • usar atributos de restrição como pattern e maxlength
    • usar o método DOM setCustomValidity da entrada
  • setCustomValidity é o recurso mais poderoso, pois pode lidar com lógica de validação arbitrária e casos complexos
  • As duas primeiras formas podem ser declaradas como atributos HTML, mas setCustomValidity exige uma chamada de método
  • Como material de referência sobre a diferença entre atributos e propriedades do DOM, há um link para Attributes vs Properties

Onde setCustomValidity começa a ficar incômodo

  • setCustomValidity é exposto apenas como método, sem um atributo HTML correspondente
  • Ao passar uma string, a entrada se torna inválida, e o navegador mostra essa string como motivo da falha de validação
  • Ao passar uma string vazia, a entrada volta a ser válida, desde que não haja outras restrições
  • Para implementar manualmente um comportamento como required, é preciso chamar setCustomValidity sempre que o valor mudar
  • Porém, a entrada começa inicialmente em estado válido, então, se o botão de envio for clicado logo após redefinir o componente, o envio do formulário pode passar
    • isso acontece porque, mesmo com o valor vazio, o código de validação não é executado antes de ocorrer um evento de mudança
    • para alinhar também o valor inicial, é necessário executar a mesma validação no momento em que o componente é montado

Mais boilerplate em componentes declarativos

  • Quando é preciso lidar também com a validação do valor inicial, o código rapidamente se torna repetitivo e desconfortável
  • Em especial, surgem três problemas
    • a lógica de validação fica duplicada entre o manipulador onChange e a etapa de renderização inicial
    • o código de validação inicial fica separado do elemento de entrada, reduzindo a coesão e aumentando o risco de alterar apenas um dos lados
    • a combinação de useRef, useLayoutEffect e onChange fica excessiva à medida que o número de entradas cresce, e causa ainda mais confusão quando apenas alguns campos usam customValidity
  • Essa complexidade fica especialmente evidente ao lidar com uma API imperativa pura dentro de componentes declarativos
  • CustomValidity não possui um atributo de entrada que permita definir seu valor declarativamente, como ocorre com os atributos nativos de validação
  • Quando a API é incômoda, a adoção pode ser baixa mesmo que o recurso seja poderoso, e esse é o principal motivo da subutilização da validação nativa de formulários

A peça que falta: o atributo custom-validity

  • O formato necessário seria um atributo declarativo como custom-validity, que pudesse ser definido diretamente na entrada
  • Em frameworks declarativos, esse atributo permitiria manter o estado de validação próximo ao elemento de entrada
  • Atualmente, a HTML Spec não possui um custom-validity real
  • É possível simular esse comportamento com uma implementação no espaço do usuário para fins de demonstração
  • Como exemplo de componente para produção, é fornecida uma implementação mais completa

Validação assíncrona e dependência entre entradas

  • A validação em aplicações reais pode ser mais complexa do que verificações locais
  • Um campo de nome de usuário precisa consultar o servidor para verificar se o nome já está em uso, e, durante a requisição, o formulário não deveria ser considerado válido
  • O exemplo usa required para impedir entradas vazias e, conforme o estado de carregamento e o resultado da resposta, usa customValidity para tornar a entrada inválida
  • A implementação é composta por duas partes
    • o estado da requisição de verificação de unicidade do nome de usuário é gerenciado com useQuery do react-query
    • é usado um componente personalizado <Input /> que pode receber a prop customValidity
  • Essa abordagem permite expressar todo o fluxo de validação assíncrona — incluindo estados de carregamento, erro e sucesso — em um único atributo
  • Um exemplo adicional mostra um formulário que exige redigitar a senha informada, tratando a validação de campos de entrada dependentes entre si

Conclusão

  • setCustomValidity é uma ferramenta poderosa, capaz de atender a uma ampla variedade de requisitos de validação
  • O que determina o uso real não é apenas a existência do recurso, mas também uma API que o torne fácil de usar
  • Uma abstração declarativa como custom-validity permitiria usar a validação nativa de formulários de forma mais natural
  • É razoável esperar que algo assim entre nativamente na especificação HTML no futuro

1 comentários

 
GN⁺ 2024-10-29
Opiniões no Hacker News
  • Da última vez que verifiquei, os navegadores web atuais ainda não permitiam estilizar a aparência das mensagens de validação HTML embutidas https://stackoverflow.com/questions/5328883/how-do-i-style-t...
    Seria menos pior se o Chrome e o Firefox pelo menos seguissem as diretrizes de UI da plataforma do SO e parecessem algo exibido pelo sistema, como um tooltip title="", mas o Chrome usa um ícone amarelo/laranja, texto preto sobre fundo branco e um balão com cantos arredondados fixos, o que entra em forte conflito com a estética do projeto atual
    O Chrome antigo permitia estilizar mensagens de validação com seletores de pseudo-elementos com prefixo de fornecedor, mas removeu esse recurso e nunca o trouxe de volta. Isso acaba entrando na lista arbitrária de irritações do tipo “me deem uma combobox HTML nativa decente” e “por que isso ainda é uma caixa difícil de usar com Ctrl+clique, e não uma lista de checkboxes?”

    • O problema aqui não é exatamente a falta de estilização customizada em si, mas o fato de ser bem fácil ler o estado nativo de validity da entrada e renderizar como quiser
      O problema real é que é complicado assinar com precisão as mudanças desse estado de validade. Existem eventos de validade, mas eles nem sempre disparam. Se você altera o estado do formulário programaticamente, como ao chamar form.reset() ou fazer input.value = '...', esses eventos não são disparados
      Acho que esse é um bom tema para investigar separadamente e propor à plataforma web
    • Não entendo muito bem por que alguém quer estilizar até isso. Expressar por cores ou layout é ótimo, mas, a partir de certo ponto, usabilidade é mais importante que branding
    • Sim. Ainda assim, dá para ocultar a mensagem padrão e substituí-la por uma mensagem própria. Mesmo assim, você mantém os benefícios da validação de formulários
    • Nesse sentido, a estilização de um select simples também não é tão fácil quanto deveria ser
  • A coisa maior, mais fácil de implementar e ainda assim subutilizada é usar valores de atributo type específicos, como email, number e url
    No mobile, isso pode abrir o teclado ideal e melhorar bastante a experiência do usuário

    • Evito type=number e uso type=text inputmode=numeric em vez disso. Ele não adiciona botões de seta, que a maioria dos usuários não precisa para inserir números, e o teclado no iOS também fica melhor
    • É surpreendente quantos lugares exibem “email” em um formulário de login e ainda assim não definem o type correto
    • Também vejo muito isso com entradas date. Cada biblioteca de frontend tem seu próprio widget de data, e muitos ficam péssimos em telas pequenas
      É preciso adicionar JS e CSS por causa daquele único widget, e alguns dependem de jQuery. Claro que alguns têm muitas configurações, mas por um custo muito pequeno você obtém um widget que é decente em qualquer lugar, parece nativo, em geral atende aos requisitos e pode ser esquecido sem se preocupar com atualizações ou CDN
    • Infelizmente, entradas number são insuficientes e inconsistentes entre navegadores. No fim, sempre voltei para validação em JavaScript
    • Recentemente tive que garantir que elementos de entrada mais complexos não fossem usados. Por vários motivos, precisávamos controlar a entrada com um teclado na tela implementado dentro da página
      Com entradas comuns isso mal é possível; entradas especiais dão suporte a ainda menos eventos
  • Meu produto não passou em uma auditoria de acessibilidade porque usava validação nativa de formulários HTML, e a recomendação oficial foi implementar uma camada própria de validação. Eu também concordo com essa recomendação
    A validação nativa do HTML tem muitas falhas e, sinceramente, a customização visual nem é a maior preocupação. Mas é quase o último prego no caixão
    Por exemplo, se você quiser mostrar vários erros por campo de uma vez, a única opção é concatenar strings. Algo como “É preciso um número. É preciso um símbolo. Deve ter mais de 10 caracteres”, o que é ruim para a experiência do usuário e também para acessibilidade. Isso porque não dá para navegar pela string concatenada na árvore de acessibilidade. Não é um problema de implementação, é um problema da própria especificação. Não é divertido para o usuário ficar brincando de caça aos erros de validação, então deveria ser possível mostrar vários erros de uma vez
    A dependência do navegador também é ruim. Você não tem controle, e as implementações em geral são péssimas. O Chrome mostra um pop-up ao receber foco; é um pop-up, parece modal e não consegue mostrar todos os erros do formulário de uma vez, então por si só já não é bom para acessibilidade. Não mostrar informações importantes em pop-ups é um princípio básico de acessibilidade, mas o navegador não tem muito mais o que fazer sem interferir no documento real
    Erros do formulário inteiro que não pertencem a um campo específico também continuam exigindo validação customizada. É o caso de erros como “os campos A e B são incompatíveis”, e, nesse caso, é melhor ter uma abordagem de validação consistente
    Se você tiver uma entrada customizada que não se encaixa nos tipos nativos de input, precisa ter um input oculto por consistência, e isso também quebra a acessibilidade. Corrigir isso acaba sendo tão difícil quanto criar um sistema próprio de validação acessível
    A API customValidity é imperativa e chata de usar. A menos que você queira mensagens horríveis como “este campo é inválido”, quase não existe a opção de não usar validade customizada. Por isso a validação de formulários HTML é péssima

    • É realmente irônico que um recurso fornecido pelo navegador possa não passar nos requisitos de acessibilidade. Afinal, sempre aprendemos que um dos motivos para usar a plataforma e seguir elementos semânticos é a acessibilidade
      Ainda assim, acho que vale a pena aproveitar o mecanismo nativo de validação. Você não precisa necessariamente usar o tooltip nativo de validade para mensagens de erro; pode ler diretamente o ValidityState de input.validity, renderizar as mensagens do jeito que quiser e mostrar vários erros se necessário
      Os navegadores podem melhorar, e usar uma abordagem padronizada permite se beneficiar disso. Uma conclusão do tipo “se você não usar pop-up, a acessibilidade quebra” soa estranha, mas, se for preciso atender à auditoria, talvez seja o caminho
      Também há técnicas para lidar com erros que não pertencem a um campo específico. Nos meus projetos, uso um componente HiddenValidationInput, que cria um input invisível e somente leitura para renderizar erros customizados que não pertencem a outros campos. Como basta renderizá-lo condicionalmente, na prática ele é bem agradável de usar
      Concordo totalmente que a API customValidity é imperativa e incômoda. Escrevi um artigo justamente sobre essa parte, e espero que isso também melhore com o tempo
    • Interessante, mas no meu app eu implemento toda essa validação. A validação do formulário inteiro é tratada no servidor. Basicamente, a abordagem é ter várias camadas de validação e aproveitar tudo o que for possível
      O design tosco e pouco acessível do Chrome é um problema do Chrome, então vou abrir um relatório de bug
      Quando você fala em “especificação”, é isto aqui? https://html.spec.whatwg.org/multipage/dom.html#concept-elem...
      Não vejo uma parte que defina como as mensagens de validação devem ser exibidas. Nem mesmo vejo algo dizendo que as mensagens precisam obrigatoriamente existir
    • Fico curioso para saber por que os auditores julgaram assim
  • Para ser sincero, parece que as pessoas que escrevem essas especificações estão desconectadas da realidade e não usam de fato aquilo que especificaram. Funciona para coisas muito simples, mas, quando os formulários começam a evoluir, você acaba percebendo que é melhor escrever tudo por conta própria

    • Sim. Mesmo uma coisa relativamente comum, como fazer referência cruzada com outro campo, quase imediatamente exige JS. Por exemplo, se o usuário informou o país, você pode validar o CEP que ele acabou de digitar; caso contrário, precisa esperar a seleção do país
      No momento em que você começa a usar JS, fica muito mais fácil fazer tudo por código do que ficar mexendo nos atributos de validação
    • Sim. É ótimo até o momento em que você precisa de um seletor de datas cross-browser; a partir daí, é preciso implementar muita coisa por conta própria. É frustrante que, depois de tantos anos, os formulários HTML ainda sejam tão primitivos
    • A maior parte dos recursos de formulário vem do começo dos anos 90. Talvez nem estejamos trabalhando no mesmo milênio que alguns dos autores da especificação
  • Se um checkbox tiver um rótulo, eu gostaria que colocassem o atributo for no rótulo para que clicar nele marque e desmarque o checkbox. Pessoalmente, é uma das coisas que mais me incomodam, mas talvez seja só comigo

    • Envolver o input com o label também funciona. Não sei muito bem por que as pessoas tendem a separar os dois
      E também não sei por que os navegadores começaram a separá-los. Acho que o texto não deveria conter checkboxes e radios; os checkboxes e radios é que deveriam conter o texto
    • Não é só você. Em sites acessíveis que seguem ADA/WCAG, isso é um recurso necessário
  • Há um exemplo simples sem React aqui
    https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectE...

    • Achei muito estranho falar de validação HTML padrão e mostrar todos os exemplos em React. Se a ideia é ensinar como o padrão funciona, não se deve presumir React como padrão
    • Hoje em dia, parece que dá para fazer a maior parte disso só com CSS. Talvez JavaScript seja necessário apenas para impedir que o pop-up padrão apareça
  • A validação de formulários HTML é excelente. Só que há uma armadilha enorme:
    ela não funciona no Firefox para Android
    https://bugzilla.mozilla.org/show_bug.cgi?id=1510450

    • Uso Firefox em todos os meus dispositivos móveis, mas é difícil culpar os desenvolvedores por ignorarem o Firefox para Android nesse caso. É uma API que todos os outros navegadores fazem funcionar há 10 anos, e a responsabilidade de resolver essa confusão é da equipe do Firefox
      Não vale a pena ignorar o padrão por causa de menos de 1% dos usuários que não conseguem ver a mensagem de erro dos controles marcados em vermelho. Quanto mais sites usarem isso, maior será a pressão para a Mozilla corrigir o navegador
      Isso não é uma API como WebMIDI, que Chrome ou Safari implementaram antes da padronização; faz parte da especificação original do HTML5
    • O Firefox para Android tem uma base de usuários menor que a do Samsung Internet ou do Opera, na casa de 0,5%. Gastar tempo com suporte a ele beira o desperdício
      Ainda mais considerando quão pouco tempo se gasta para garantir que pessoas que usam software de acessibilidade consigam usar os sites corretamente. A menos que você esteja pronto para falar até do UC Browser, não acho que valha muito a pena mencionar esse tipo de problema
    • Como alguém que usa Firefox diariamente no Android, o que mais dói é não acompanhar os padrões. Especialmente problemas relacionados a WebGL como [1] e pequenos incômodos como [2]
      Ainda assim, poder usar o uBlock Origin junto com outras extensões é uma vantagem poderosa
      [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1884282
      [2] https://bugzilla.mozilla.org/show_bug.cgi?id=1897707
    • É muito pior do que “não funciona”. A validação funciona, mas não mostra nenhum erro
      Se a validação simplesmente não tivesse sido implementada, até seria melhor; isso é uma verdadeira bagunça. Descobri isso há alguns anos, depois de uma depuração longa e dolorosa, e tenho certeza de que não fui a primeira nem a última pessoa a passar pela mesma coisa
    • Só percebi isso recentemente, depois de mudar para o Firefox no Android. Enquanto criava um app, ao tentar enviar um campo obrigatório vazio, literalmente nada aparecia
      Até pesquisar, eu não conseguia nem imaginar o que estava deixando passar, e foi bem chocante. Criar sua própria validação tudo bem, mas isto deveria funcionar
  • É preciso tomar cuidado para não exagerar no uso
    Recentemente tentei obter um reembolso no Groupon. A empresa para a qual eu havia comprado o Groupon passou para uma nova gestão e não aceitou meu Groupon
    O formulário tinha a condição de “no mínimo 15 palavras”, mas, por mais que eu tentasse, não conseguia passar na validação, então acabei tendo que inspecionar o HTML
    \w era caractere de palavra, \b era limite de palavra, \s era espaço em branco, e pontuação literalmente não era permitida de forma alguma

    • Não acho que esse seja exatamente um problema exclusivo da validação HTML. O mesmo acontece com qualquer tipo de validação. As mesmas regras poderiam ter sido aplicadas no servidor e, nesse caso, não haveria esperança
    • Isso também é uma boa evidência do ponto que eu queria levantar. Atributos de validade existentes, como pattern, são legais, mas não bastam
      Por exemplo, o caso de “repetir a senha” pode ser implementado com o atributo pattern, sem setCustomValidity. Para isso, seria preciso construir dinamicamente uma expressão regular a partir do primeiro valor digitado
      Não comparei as soluções para o texto não ficar longo demais, mas o ponto principal é que usar customValidity torna a validação muito mais clara e legível. Aqui, uma API melhor faz uma grande diferença
      A restrição de “no mínimo 15 palavras” também poderia ser expressa de forma bem mais legível, como value.split(/\s+/).length >= 15
  • Há um motivo para ser pouco usado. Muitos frameworks e bibliotecas oferecem recursos de validação robustos e passíveis de estilização, e alguns são muito sofisticados e extensíveis. Se não é preciso sofrer, é melhor não se torturar

    • De qualquer forma, você também precisa implementar a validação no backend. Sempre há alguém tentando mexer no formulário com um navegador estranho, curl ou alguma outra ferramenta que não tenha a mesma validação de formulário embutida
      Não dá para confiar que o cliente fez a validação, nem que a fez corretamente, então o backend ainda precisa validar a entrada e ser capaz de exibir o formulário com os erros de validação de todos os campos
      A validação no frontend serve apenas para ajudar o usuário. Mas, se você quiser estilizar ou disparar a partir do backend no momento do envio, acaba tendo que implementar sua própria estilização de qualquer jeito
    • Concordo. Tento usar recursos padrão do navegador quando possível, mas a validação embutida nunca se mostrou valiosa quando fui avaliá-la
      Há armadilhas demais e, para dar suporte fácil a verificações mais complexas, você acaba usando uma biblioteca
      Além disso, ao usar uma biblioteca, dependendo do caso, também surge a possibilidade de compartilhar parte do código de validação entre frontend e backend
      Em especial, este artigo parece contornar um problema com useLayoutEffect, o que não é algo para se fazer de forma leviana
  • Uma das coisas de que não gosto na validação de formulários HTML é que ela roda desde o carregamento da página. Por exemplo, se você vincular estilos de estado de erro a isso, o formulário pode carregar já cheio de erros desde o início, parecendo intimidador para o usuário

    • Existe a pseudo-classe :user-invalid, que ajuda a evitar isso até certo ponto. Mas ela tem partes pouco flexíveis, então pode não ser suficiente dependendo do caso de uso
      https://developer.mozilla.org/en-US/docs/Web/CSS/:user-inval...
    • Esse é um dos principais motivos pelos quais as pessoas começaram a usar JavaScript: mostrar erros apenas depois que o formulário foi “tocado” ou logo antes do envio
    • Nunca entendi por que isso foi escolhido como experiência padrão. A maioria dos usuários não gosta de ver todos os elementos do formulário gritando com eles quando ainda nem tiveram a chance de agir
      Dá para contornar com script, mas, nesse ponto, esse recurso já não faz muita coisa por você. Acho que esse é o maior motivo para ele não ser adotado mais amplamente