Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 10 — Validação de Formulários com Vee-Validate 4 e Schemas Zod

Este exemplo apresenta a construção de formulários corporativos robustos no Vue 3 com validação declarativa através do vee-validate e integração com esquemas tipados do zod (toTypedSchema).

<!-- src/components/SystemRegisterForm.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import * as z from 'zod';

// 1. Definição do Esquema de Validação Estrito com Zod
const registrationSchema = z.object({
  fullName: z
    .string({ required_error: 'O nome completo é obrigatório.' })
    .min(3, 'O nome deve possuir no mínimo 3 caracteres.')
    .max(80, 'Limite de 80 caracteres excedido.'),
  email: z
    .string({ required_error: 'E-mail corporativo é obrigatório.' })
    .email('Formato de e-mail inválido.')
    .endsWith('@empresa.com.br', 'Apenas e-mails do domínio @empresa.com.br são aceitos.'),
  password: z
    .string({ required_error: 'A senha é obrigatória.' })
    .min(8, 'A senha deve conter ao menos 8 caracteres.')
    .regex(/[A-Z]/, 'Deve conter pelo menos uma letra maiúscula.')
    .regex(/[0-9]/, 'Deve conter pelo menos um dígito numérico.'),
  termsAccepted: z
    .literal(true, { errorMap: () => ({ message: 'Você deve aceitar os termos de serviço.' }) })
});

type RegistrationFormData = z.infer<typeof registrationSchema>;

// 2. Inicialização do Formulário com Vee-Validate
const { defineField, handleSubmit, errors, isSubmitting, resetForm, meta } = useForm<RegistrationFormData>({
  validationSchema: toTypedSchema(registrationSchema),
  initialValues: {
    fullName: '',
    email: '',
    password: '',
    termsAccepted: false
  }
});

// 3. Vinculação com defineField para props reativas de inputs
const [fullName, fullNameProps] = defineField('fullName');
const [email, emailProps] = defineField('email');
const [password, passwordProps] = defineField('password');
const [termsAccepted, termsAcceptedProps] = defineField('termsAccepted');

const submissionSuccess = ref<boolean>(false);

// 4. Submissão protegida: só é disparada se o formulário for 100% válido
const onSubmit = handleSubmit(async (values) => {
  submissionSuccess.value = false;
  // Simulação de chamada HTTP POST para a API
  await new Promise((resolve) => setTimeout(resolve, 1000));

  console.log('[Form Submission] Usuário cadastrado com sucesso:', values);
  submissionSuccess.value = true;
  resetForm();
});
</script>

<template>
  <div class="max-w-md mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-6">
    <div>
      <h2 class="text-xl font-bold text-slate-800">🔒 Cadastro de Operador de Sistema</h2>
      <p class="text-xs text-slate-500 mt-1">Validação em tempo real orientada por Esquemas Zod.</p>
    </div>

    <div
      v-if="submissionSuccess"
      class="p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-xl text-sm"
    >
      ✅ Conta criada com sucesso! Verifique seu e-mail corporativo para ativação.
    </div>

    <form @submit="onSubmit" class="space-y-4" novalidate>
      <!-- Campo Nome Completo -->
      <div class="space-y-1">
        <label class="block text-xs font-semibold text-slate-700">Nome Completo:</label>
        <input
          type="text"
          v-model="fullName"
          v-bind="fullNameProps"
          placeholder="Ex: Ana Maria Silva"
          class="w-full border px-3 py-2 rounded-lg text-sm transition-colors outline-none focus:ring-2 focus:ring-indigo-500"
          :class="errors.fullName ? 'border-red-400 bg-red-50/50' : 'border-slate-300'"
        />
        <p v-if="errors.fullName" class="text-xs text-red-600 font-medium">{{ errors.fullName }}</p>
      </div>

      <!-- Campo E-mail Corporativo -->
      <div class="space-y-1">
        <label class="block text-xs font-semibold text-slate-700">E-mail Corporativo:</label>
        <input
          type="email"
          v-model="email"
          v-bind="emailProps"
          placeholder="nome@empresa.com.br"
          class="w-full border px-3 py-2 rounded-lg text-sm transition-colors outline-none focus:ring-2 focus:ring-indigo-500"
          :class="errors.email ? 'border-red-400 bg-red-50/50' : 'border-slate-300'"
        />
        <p v-if="errors.email" class="text-xs text-red-600 font-medium">{{ errors.email }}</p>
      </div>

      <!-- Campo Senha -->
      <div class="space-y-1">
        <label class="block text-xs font-semibold text-slate-700">Senha de Acesso:</label>
        <input
          type="password"
          v-model="password"
          v-bind="passwordProps"
          placeholder="Mínimo 8 chars, 1 maiúscula, 1 número"
          class="w-full border px-3 py-2 rounded-lg text-sm transition-colors outline-none focus:ring-2 focus:ring-indigo-500"
          :class="errors.password ? 'border-red-400 bg-red-50/50' : 'border-slate-300'"
        />
        <p v-if="errors.password" class="text-xs text-red-600 font-medium">{{ errors.password }}</p>
      </div>

      <!-- Checkbox Termos -->
      <div class="space-y-1">
        <label class="flex items-start gap-2 text-xs text-slate-700 cursor-pointer pt-1">
          <input
            type="checkbox"
            v-model="termsAccepted"
            v-bind="termsAcceptedProps"
            class="rounded text-indigo-600 mt-0.5"
          />
          <span>Declaro ciência dos termos de uso da infraestrutura corporativa.</span>
        </label>
        <p v-if="errors.termsAccepted" class="text-xs text-red-600 font-medium">{{ errors.termsAccepted }}</p>
      </div>

      <!-- Botão de Ação com Estados de Dirty / Submitting -->
      <button
        type="submit"
        :disabled="isSubmitting || !meta.valid"
        class="w-full py-2.5 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
      >
        <span v-if="isSubmitting" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
        {{ isSubmitting ? 'Registrando...' : 'Confirmar Cadastro' }}
      </button>
    </form>
  </div>
</template>

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Formulário limpo. Botão "Confirmar Cadastro" desabilitado (meta.valid é false).

Digitação com Erro de Domínio:
Nome: "Ricardo Lima"
E-mail: "ricardo@gmail.com"
Mensagem de Erro sob o E-mail: "Apenas e-mails do domínio @empresa.com.br são aceitos."
Borda do campo muda para vermelha.

Correção do E-mail e Senha:
E-mail: "ricardo@empresa.com.br" -> Erro desaparece.
Senha: "senha" -> Erro: "A senha deve conter ao menos 8 caracteres."
Senha: "Password123" -> Erros de maiúscula e número sanados.
Marcar checkbox de termos.

Envio:
meta.valid torna-se true. Botão é habilitado.
Ao submeter:
Botão exibe spinner com "Registrando...".
Após 1000ms:
Banner verde: "✅ Conta criada com sucesso!".
Campos limpos via resetForm().

🔍 Destaques da Implementação

  1. Schema-First Design: A lógica de validação reside integralmente no schema Zod desacoplado da interface gráfica.
  2. defineField (Vee-Validate 4.10+): Gera automaticamente bindings bidirecionais sem a verbosidade de wrappers de terceiros.
  3. Impedimento de Submissão Inválida: O callback passado a handleSubmit só executa se todos os 4 campos passarem pelas checagens semânticas.