Hey dev, usando Zod no Angular? Pra que?
No mundo do desenvolvimento javascript, a biblioteca Zod é uma poderosíssima ferramenta para validação de dados e formularios. Não seria nada legal salvar um NaN ou undefined no banco de dados né? né????
Pois bem, temos no mundo do React o costume de usar bibliotecas para tudo (afinal, React é uma casca vazia), e uma delas é o poderoso Zod para validação de formularios.
Observe o exemplo:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Definindo o esquema de validação com Zod
const schema = z.object({
nome: z.string().min(2, 'Muito curto').max(50, 'Muito longo'),
sobrenome: z.string().min(2, 'Muito curto').max(50, 'Muito longo'),
senha: z.string().min(8, 'A senha deve ter pelo menos 8 caracteres'),
repetirSenha: z.string().refine((value) => value === senha, { message: 'As senhas precisam ser iguais' }),
});
export const Formulario = () => {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Nome:</label>
<input {...register("nome")} />
{errors.nome && <p>{errors.nome.message}</p>}
</div>
<div>
<label>Sobrenome:</label>
<input {...register("sobrenome")} />
{errors.sobrenome && <p>{errors.sobrenome.message}</p>}
</div>
<div>
<label>Senha:</label>
<input type="password" {...register("senha")} />
{errors.senha && <p>{errors.senha.message}</p>}
</div>
<div>
<label>Repetir Senha:</label>
<input type="password" {...register("repetirSenha")} />
{errors.repetirSenha && <p>{errors.repetirSenha.message}</p>}
</div>
<button type="submit">Enviar</button>
</form>
);
};
Podemos ter a mesma validação no backend na hora de receber um body http e vários outros exemplos. Mas porque citei o React? Porque o assunto está caminhando para o framework frontend Angular.
Então vamos lá, vamos criar um componente Angular independente usando um template string para montar o nosso html.
@Component({
selector: 'app-formulario',
standalone: true,
imports: [CommonModule, ReactiveFormsModule], //importando o ReactiveFormsModule no componente
template: `
<form
(submit)="enviarFormulario"
[formGroup]="form" // injetando o atributo form no formulario
*ngIf="form" // Utilizando o ngIf aqui para evitar erro ExpressionChangedAfterItHasBeenCheckedError
>
<div>
<label>Nome:</label>
<input type="text"
formControlName="nome" //Passando aqui um controle reativo no formulario
/>
</div>
<div>
<label>Sobrenome:</label>
<input type="text" formControlName="sobrenome" />
</div>
<div>
<label>Senha:</label>
<input type="password" formControlName="senha" />
</div>
<div>
<label>Repetir Senha:</label>
<input type="password" formControlName="repetirSenha" />
</div>
<button type="submit">Enviar</button>
</form>
`,
})
export class FormularioComponent {
public form!: FormGroup;
}
Ótimo, agora, pro nosso componente não fica muito extenso, vamos trabalhar dentro da classe omitindo o html. Mas lembre-se da regra dos decoratos senhor copia e cola que caiu aqui no desespero, decorator tem que ficar acima de uma classe.
Vamos começar instanciando o nosso grupo de formulario usando a classe FormGroup
. Dentro do objeto, vamos usar um objeto onde os valores são instancias de FormControl
.
Ficou dificil? Vamos tirar a prova no código!
obs: instanciamos os elementos no hook onInit
@Component({...})
export class FormularioComponent {
public form!: FormGroup;
ngOnInit(): void {
this.form = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(2)]),
sobrenome: new FormControl('', [Validators.required, Validators.maxLength(50)]),
senha: new FormControl('', [Validators.minLength(8), Validators.pattern(Regex)]),
repetirSenha: new FormControl('', [Validators.required, Validators.minLength(8)]),
})
}
}
Pronto! Agora nos temos um formulario reativo que acrecenta em cada atributo o input do formulário de forma reativa. Usamos também a classe estatica Validators
para usar validação nos campos de formulário.
Para encerar, vamos criar uma validação personalizada para verificar se o campo senha e repetirSenha são iguais.
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export const MustMatch = (value: string, valueMatch: string): ValidatorFn | null => {
return (control: AbstractControl): ValidationErrors | null => {
const match = control.get(value);
const valueToMatch = control.get(valueMatch);
if (match?.value !== valueToMatch?.value) {
valueToMatch?.setErrors({ mustMatch: true });
return { mustMatch: true };
}
return null;
};
};
Agora vamos implementar no componente utilizando como segundo argumento de FormGroup.
@Component({...})
export class FormularioComponent {
ngOnInit(): void {
this.form = new FormGroup({...}, { validators: MustMatch('senha', 'repetirSenha') })
}
}
Ótimo, agora podemos acessar diretamente os erros de cada campo diretamente no html usando os getters da instancia de FormGroup.
<div *ngIf="form.get('password')?.invalid && form.get('senha')?.touched">
<div *ngIf="form.get('senha')?.errors?.['required']">Senha não pode ficar em branco</div>
<div *ngIf="form.get('senha')?.errors?.['minlength']">Senha muita curta</div>
<div *ngIf="form.get('senha')?.errors?.['mustMatch']">Senhas não são iguais </div>
<div *ngIf="form.get('senha')?.errors?.['pattern']">Senha não atende os requisitos </div>
</div>
É isso. O Angular faz o resto. E não precisamos instalar uma nova dependencia para isso.
Fale comigo na comunidade que participo
No Angular não é bom usar o Zod para validação de formulário justamente porque ali já temos meios nativos de se fazer isso.
Mas isso não quer dizer que o Zod seja descartável em um projeto Angular.
O Zod brilha em lugares como, por exemplo, respostas de APIs. Imagina que sua app espera receber um JSON assim:
{ "id": 1, "name": "Fulano da Silva", "active": 2 }
E vc constrói toda sua lógica esperando receber um ID numérico e uma propriedade active
com os valores 2 para "ativo" e 1 para "inativo".
type User = { id: number; name: string; active: 1 | 2 };
@Injectable({ providedIn: "root" })
export class UserService {
getAll(): Observable<User[]> {
// Faz de conta que tá vindo do HttpClient 😉
return of([
{ id: 1, name: "João", active: 2 },
{ id: 2, name: "Maria", active: 1 },
]);
}
}
Num belo dia, os devs da API decidem que agora a propriedade active
deve ser um booleano e que o ID seria um UUID. Pronto! Sua lógica pode falhar.
Em algum momento vc vai perceber que a falha não vem da sua app, mas da resposta da API, mas com o Zod vc descobriria logo de cara onde o problema vem.
const userSchema = z.object({
id: z.number(),
name: z.string(),
active: z.union([z.literal(1), z.literal(2)]),
});
type User = z.infer<typeof userSchema>;
// ...
getAll(): Observable<User[]> {
// Faz de conta que tá vindo do HttpClient 😉
return of([
{
id: "c2fa20d9-e248-4e96-b003-904fc13a3e9f",
name: "João",
active: true,
},
{
id: "3ae4e6bf-9622-400e-bda7-5ce110519382",
name: "Maria",
active: false,
},
] as any).pipe(map((e) => userSchema.array().parse(e)));
}
Gostei muito do post, obrigado por tazer. Estou começando a usar Angular em meus projetos pessoais. Adoraria ver mais sobre ele por aqui.