💻 Exemplos: Capítulo 10: Formulários Avançados e Validação com Vee-Validate e Zod
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
- Schema-First Design: A lógica de validação reside integralmente no schema Zod desacoplado da interface gráfica.
defineField(Vee-Validate 4.10+): Gera automaticamente bindings bidirecionais sem a verbosidade de wrappers de terceiros.- Impedimento de Submissão Inválida: O callback passado a
handleSubmitsó executa se todos os 4 campos passarem pelas checagens semânticas.