Como criar formulários sem ter milhões de dependências externas?
Porque existem várias libs de formulários? Porque existem sempre tantas complicações quando se trata de formulários?
A maioria do tempo trabalhamos com formulários simples (que serão o foco desse artigo) e raramente encontramos casos onde existem objetos aninhados ou listas (tópico para o próximo artigo). Se a maioria dos casos são simples, por que utilizamos várias e várias bibliotecas?
No mundo react você irá encontrar vários e vários artigos como este, falando sobre forms controlados (controlled forms)e forms não controlados (uncontrolled forms). A ideia aqui não é trazer mais do mesmo, mas sim fazer uma comparação mais profunda (ou o famoso deep dive) em ambas as formas
Como o nome diz, são forms controlados. Mas controlados por o que? Nesse caso, controlados pelo estado do React. Neste caso você terá um useState ou useReducer para sincronizar o estado do react com os seus formulários na tela.
Particularmente, eu costumo evitar essa forma quando os formulários são simples. Mas esse método é bem útil nas seguintes situações
Tendo em mente feito o controle de estado, podemos aplicar a lógica reativa do react ao nosso formulário de forma bem simples
Essa talvez seja a implementação mais simples para formulários controlados, talvez a única coisa complexa aqui seja o handler para as mudanças de estado.
No exemplo a seguir iremos implementar as seguintes features:
<form>
com um onSubmit aplicado para a lógica da função citada acima1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
type State = { name: string; country: string; }; export default function FormPage() { const [state, setState] = useState({}); const onChange = (event: React.ChangeEvent<HTMLInputElement>) => { const {name, value} = event.currentTarget; // vale lembrar que value sempre será uma string setState(prev => ({...prev, [name]: value})); } const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const form = event.currentTarget; console.log(state); } return ( <form onSubmit = {onSubmit} > <input name = "name" value = {state.name} /> < input name = "country" value = {state.country} /> < button type = "submit" > Submit < /button> < /form> ) ; }
Com esse simples código, você irá conseguir fazer formulário simples com o controle de estado. Sinta-se a vontade para acrescentar quaisquer lógicas customizadas, sejam utilizando um useEffect ou quaisquer event listeners do seu input, tais como onBlur e onFocus.
Para casos onde você não tem uma validação customizada, esse approach é perfeito porquê:
Claro que assim é simplista demais, mas você pode fazer o uso da [[#^ada748|Validity State]] para garantir algumas consistências como valor numérico, min e max, range, checkbox ou radiobox. E o melhor de tudo, isso é nativo do navegador. Mas daqui a pouco vamos ver melhor esses exemplo com validity State
Como o próprio nome diz, forms não controlados não possuem controle de estado. A captura dos valores desse tipo de
formulário é toda feita no submit do formulário. A captura pode ser feita através de uma lógica de parsear todos os
inputs do formulário através de querySelectorAll ou form.elements
, ou ainda
utilizando FormData.
Iremos fazer das duas formas para que fique bem claro algumas das possibilidades
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
type State = { name: string; country: string; }; export default function App() { const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const form = event.currentTarget; const data = new FormData(form); const state = Object.fromEntries([...data.entries()]) as State; console.log(state); }; return ( <form onSubmit = {onSubmit} > <input name = "name" / > <input name = "country" / > <button type = "submit" > Submit < /button> < /form> ); }
Bem simples, não é mesmo? E o melhor de tudo é que tudo isso é nativo do navegador, zero controle de estado durante a interação do usuário e total controle dos dados na ação de submit.
Apesar de utilizar uma API que trata nativamente os dados do formulário, mesmo que você utilize um type=number
, o
FormData não irá fazer a conversão automática :/
Um recurso bem famoso para selecionar elementos é o ELEMENT.querySelector
ou ELEMENT.querySelectorAll
. A diferença
entre os dois é que o querySelectorAll
retorna um NodeListOf
dos elementos HTML, e não, isso não é um array para
você utilizar métodos como
.filterou
.reduce`.
O uso do querySelector é bem simples, basta escrever
um CSS Selector e você terá
um NodeListOf
desses elementos.
Sem mais delongas, vamos para o código
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
type State = { name: string; country: string; }; export default function App() { const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const form = event.currentTarget; const state = [...form.querySelectorAll("input")].reduce<State>((acc, el) => ({ ...acc, [el.name]: el.value }), {}) }; return ( <form onSubmit = {onSubmit} > <input name = "name" / > <input name = "country" / > <button type = "submit" > Submit < /button> < /form> ); }
Assim como o método anterior, esse método também não faz nenhum controle de estado durante as ações do usuário, apenas na ação de submit você terá acesso a todos os valores preenchidos no formulário.
A diferença básica entre os métodos é que nesse método você irá fazer a seleção "manual" do que contemplará o estado. Como é um CSS Selector, você pode fazer queries mais complexas baseadas em dataset ou AriaAttributes.
Até agora já foram apresentados 3 métodos de formulário e nenhum deles foi devidamente validado. Mas isso nós vamos ver agora...
Essa é uma das APIs mais subestimadas do navegador. Pouquissimo se usa ela em detrimento de bibliotecas de validação como Yup ou Zod junto de alguma outra lib de validação como react-hook-form e similares.
Esse combo de bibliotecas é até interessante, mas talvez em situações onde vc queira manter um tamanho de build menor,
eles não vão ser tão efetivos assim. E é exatamente aqui onde
a Validity State brilha. E vale lembrar que você só
pode utilizar ela com inputs dentro da tag <form/>
, caso contrário, nenhuma validação será feita
Claro que em alguns casos mais complexos de formulário como:
Enfim...qualquer lógica um pouco mais complexa ela costuma não lidar bem com a Validity State. Mas nada impede seu uso, basta pensar num fluxo amigável para o usuário que dispense a necessidade de dependência entre campos ou objetos/listas complexas.
Com a ValidityState, podemos aplicar CSS baseado no estado do nosso elemento. Temos também algumas "razões" para os
motivos dos erros, o que nos facilita o entendimento e o controle de validação do componente. Ao total são 10 estados de
erro e 1 estado de válido, chamado :valid
.
:invalid
, aplicado para casos onde não existe valor:invalid
, aplicado para casos onde o atributo type(email ou url) possui um formato
incorreto em seu valor:invalid
ou out-of-range
, aplicado para casos onde o valor não possui a quantidade de
caracteres mínima. Controle feito através de minLength
maxLength
step
. Caso não seja, trigga o
estado de :invalid
ou out-of-range
.tooShort
, porém para <input type=number/>
tooLong
, porém para <input type=number/>
:invalid
quando o valor do input não corresponde ao padrão determinado
em patternE caso nenhum desses seja considerado como verdadeiro, significa que nosso input está indeterminado ou válido. Em casos
de válido, o estado :valid
será triggado. O caso
de indeterminado é aplicado para valores iniciais,
checkbox ou radiobox.
Conhecendo esses valores você pode criar estilos utilizando seletores CSS baseados no estado do seu input e reduzir a quantidade de lógica no seu código Javascript
Com todo o conteúdo apresentado, fica um pouco mais fácil decidir o que fazer quando esbarrar em alguma situação de formulário. Não é preciso adicionar libs para validar alguns campos, só em situações que são realmente complexas.
Uma mentalidade legal de adotar, não só para formulários, é utilizar mais do navegador ao invés de utilizar soluções custom. Isso reduz a quantidade de código entregue para o cliente e melhora a experiência, trazendo uma experiência mais nativa/familiar.
É isso, espero que tenham curtido e até a próxima.