Executando verificação de segurança...
2

Menos dependências, mais controle: Uma Abordagem Minimalista na Validação de Formulários em Aplicações Vue.js

1. Validação de Formulários: Equilíbrio entre Conveniência e Controle

A validação de formulários é um requisito fundamental no desenvolvimento web. Garantir que os dados inseridos pelo usuário atendam a regras predefinidas é crucial para a integridade do sistema.

Em projetos pequenos, a validação "manual" pode ser suficiente, mas, à medida que o projeto escala, a complexidade aumenta e a manutenção se torna insustentável. É nesse contexto que surgem as bibliotecas especializadas na validação de formulários. Elas oferecem padrões, organizam o código e, ao menos em teoria, simplificam o processo.

No ecossistema Vue.js, a biblioteca VeeValidate é extremamente popular. No entanto, essa conveniência tem um custo duplo: o aumento da dependência externa e a opacidade do código.

A dependência excessiva de bibliotecas de terceiros é um problema comum na web moderna. Mesmo projetos simples se tornam inchados. Cada nova biblioteca adiciona alguns fatores de risco:

  • Segurança: Aumenta a superfície de ataque (lembre-se dos ataques supply chain).
  • Manutenibilidade: A opacidade da implementação interna pode dificultar a correção de bugs (é nesse momento que alguns workarounds surgem).
  • Longevidade: O projeto fica refém da maturidade, suporte e atualizações da dependência (lembre-se da primeira versão do Angular.js).

Para projetos complexos, uma base sólida e controlada é vital.

Empresas maduras implementam processos bem estruturados de análise de licença de uso e politica de atualização de segurança das bibliotecas em uso, mas a primeira análise deve ser: a real necessidade e o impacto delas no projeto a longo prazo.

2. A Regra de Ouro da Dependência: Adicionar Bibliotecas com Máxima Responsabilidade

O desenvolvimento eficiente começa com uma análise crítica da necessidade. O objetivo é entender profundamente o problema e buscar a solução mais eficiente e suficiente para ele. Isso significa que, se uma biblioteca de terceiros for a maneira mais robusta, segura e sustentável de resolver um determinado problema, ela deve ser utilizada. Contudo, a regra deve ser: adicionar dependências com máxima responsabilidade. Essa abordagem elimina a dependência excessiva e a opacidade da implementação em um cenário onde a validação pode ser simplificada.

3. A Inspiração: O Conceito do VeeValidate (Composition API)

Para construir nossa solução minimalista, nos inspiramos no Composition API do VeeValidate, que nos permite o uso de elementos de formulário HTML padrões (sem a obrigação de utilizar elementos customizados).

Dois conceitos centrais do VeeValidate:

  1. "Formulário Virtual" (useForm): Um objeto central que concentra e gerencia todos os campos (fields).
  2. "Definição de Campo" (defineField): Uma função que cria a variável que será vinculada ao input real, gerenciando regras e estado de erro.

Exemplo de uso do vee-validate em uma aplicação Vue.js

<script setup>
import { useForm } from 'vee-validate';
import * as yup from 'yup';

const { values, errors, defineField } = useForm({
  validationSchema: yup.object({
    email: yup.string().email().required(),
  }),
});

const [email, emailAttrs] = defineField('email');
</script>

<template>
  <input v-model="email" v-bind="emailAttrs" />

  <pre>values: {{ values }}</pre>
  <pre>errors: {{ errors }}</pre>
</template>

4. A Implementação Minimalista: Os Componentes Essenciais

Diferentemente de uma solução framework-like como o VeeValidate, que traz consigo um ecossistema completo (suporte a schema builders como Yup/Zod e componentes customizados), concentramos nosso esforço na interface mínima necessária para registrar e validar o estado de cada campo.

Nossa biblioteca (lib) de validação se resume a quatro elementos:

createForm()

Cria um objeto (o "formulário virtual") que armazena múltiplos campos. Oferece métodos para adicionar novos campos, validar o formulário inteiro e redefinir (resetar) todos os erros de uma só vez.

Implementação da função createForm

export function createForm() {
  const fields: Field[] = []
  return {
    addField: (name: string) => {
      const field = defineField(name)
      fields.push(field)
      return field
    },
    isValid: () => {
      for (const field of fields) {
        if (!field.isValid()) {
          return false
        }
      }
      return true
    },
    reset: () => {
      for (const field of fields) {
        field.reset()
      }
    },
  }
}

defineField(name)

Cria e retorna um objeto (o "campo de formulário") com nome, estado de erro reativo e métodos para validação. Permite adicionar regras, validar valores, redefinir (resetar) erros e verificar se o campo está válido.

Implementação da função defineField

export function defineField(name: string) {
  let _rules: Rule[] = []
  const error = ref('')
  return {
    name,
    error,
    addRules: (rules: Rule[]) => _rules = rules,
    validate: (value: string|number|boolean|null) => {
      error.value = ''
      for (const rule of _rules) {
        const result = rule(value)
        if (typeof result === 'string') {
          error.value = result
          break
        }
      }
      return error.value
    },
    reset: () => error.value = '',
    isValid: () => !error.value,
  }
}

Field

Define a estrutura de um campo de formulário validável. Contém o nome do campo, um estado reativo de erro (Ref string), e métodos para gerenciar regras e validações:

  • addRules: adiciona ou substitui as regras associadas ao campo.
  • validate: executa as regras sobre o valor e atualiza o erro, retornando a mensagem se houver.
  • reset: limpa o erro atual do campo.
  • isValid: indica se o campo está válido, retornando true quando não há erro.

Declaração do tipo Field

export type Field = {
  name: string,
  error: Ref<string>,
  addRules: (rules: Rule[]) => void,
  validate: (value: string) => string,
  reset: () => void,
  isValid: () => boolean,
}

Rule

Representa uma função de validação que recebe um valor (por exemplo: string, number, boolean, ou null) e retorna true se o valor for válido ou uma string contendo a mensagem de erro caso seja inválido. É a base para todas as regras de validação da biblioteca.

Declaração do tipo Rule

export type Rule = (value: string|number|boolean|null) => boolean|string

length(value, message?)

Cria uma regra de validação que verifica se o valor fornecido é uma string com exatamente o número de caracteres especificado por value. Se o comprimento for diferente, retorna a mensagem de erro (personalizada ou padrão).

É útil para validar campos com tamanho fixo, como CPF, CNPJ ou CEP formatados.

Exemplo de função de validação

export function length(value: number, message: string = `Deve ter exatamente ${value} ${(value > 1 ? 'caracteres' : 'caractere')}`) {
  return (v: string|number|boolean|null) => (typeof v === 'string' && v.length === value) || message
}

5. Aplicação Prática: O Fluxo em um Componente Vue

Este exemplo demonstra como aplicar a biblioteca de validação dentro de um componente Vue.

Inicialmente, um formulário é criado com createForm(), e um campo chamado message é adicionado. Esse campo recebe as regras de validação: obrigatório (required()), comprimento mínimo (minLength(1)) e máximo (maxLength(100)). A função validateMessage é chamada sempre que o usuário digita (método @input) ou sai do campo (método @blur ou @change), garantindo a validação em tempo real.

No template, o valor do campo é vinculado à variável reativa message, e as mensagens de erro são exibidas abaixo do input. Por fim, a função validateForm() pode ser utilizada no momento do envio para revalidar todos os campos do formulário antes de prosseguir.

Exemplo de uso da biblioteca criada

<script setup lang='ts'>
import { ref } from 'vue';
import { createForm, maxLength, minLength, required } from '@/validator/validator';

const message = ref('')

const form = createForm()
const messageField = form.addField('message')

messageField.addRules([required(), minLength(1), maxLength(100)])

const validateMessage = () => messageField.validate(message.value)

// Utilizado no método @submit para revalidar todos os campos no momento do envio
function validateForm() {
  validateMessage()
}

</script>

<template>
  <input v-model="message" @input="validateMessage" @blur="validateMessage"></input>
  <pre>{{ messageField.error }}</pre>
</template>

6. Conclusão: Priorizando o Controle

Essa abordagem nos entrega leveza, simplicidade e controle. Caso um bug ocorra ou uma nova regra de validação precise ser adicionada, o desenvolvedor possui todas as ferramentas para fazer uma correção ou implementação rápida.

Em quais cenários não faz sentido?

Para projetos com validações extremamente complexas, onde campos dependem de outros, uma biblioteca madura com suporte a schema builders, pode, de fato, economizar tempo de desenvolvimento (o time-to-market).

Em resumo: priorize a simplicidade e o controle sempre que possível.

Um esforço inicial para criar uma solução eficiente e suficiente fará toda a diferença na longevidade, performance e manutenabilidade do seu projeto. Sempre que possível seja dono do seu código.

A verdadeira lição aqui não reside na construção de uma biblioteca em si, mas sim no apelo ao pensamento crítico aplicado no processo de desenvolvimento de software.

Link para a biblioteca no GitHub

Carregando publicação patrocinada...
1

Legal, primeiro parabéns pelo projeto! Já deixei uma estrelinha no Github.

Eu recentemente migrei a tecnologia de componentes de formulários do meu trabalho de FormKit para PrimeVue. Durante esse processo tive que refazer toda a lógica de validação que estava presente no FormKit...e vou te falar, foi bem mais rápido e menos doloroso do que eu pensava que seria. Acabei eu mesmo escrevendo (com boost de IA) toda a logica de validação em um composable.

Então para ser bem sincero, eu não utilizaria essa e qualquer outra biblioteca de validação ou criação de formulários. Eu correspondi bastante com sua declaração:

...adicionar dependências com máxima responsabilidade

Tenho seguido nessa tendência cada vez mais e hoje não coloco validações como uma Adependência que vale a pena na maioria dos casos. Nem bibliotecas de componentes eu tenho gostado muito de utilizar

Por que deixei a estrela? Porque o processo de criação é um aprendizado bem valioso!

1

Obrigado Arthur.
Você pegou a ideia.
Utilizei a criação da biblioteca em si, somente para ilustrar a importância do pensamento crítico e da responsabilidade no uso de código de terceiros.
Grande abraço!