Como validar um intervalo de data utilizando um custom validator do ReactiveForms do Angular 2+
Objetivo e construção do ambiente
O objetivo é construir um formulário com dois campos:
- data inicial
- data final
Ao término do desenvolvimento o botão Enviar deverá ser habilitado apenas se a diferença entre a data inicial e a data final for menor que 7 dias.
A título de conhecimento, em JavaScript conseguimos efetuar este cálculo da seguinte forma:
const a = new Date(2021, 0, 1);
const b = new Date(2021, 0, 5);
console.log((b.getTime() - a.getTime()) / (1000 * 60 * 60 * 24));
// Resultado: 4
O desenvolvimento do formulário ficará no AppComponent
que pertence ao módulo AppModule
. Como vamos trabalhar com Reactive Forms será necessário importar o módulo ReactiveFormsModule
:
// app.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
],
imports: [
AppRoutingModule,
BrowserModule,
CommonModule,
ReactiveFormsModule, // <<< Se vamos trabalhar com
// reactive forms, é melhor
// importar seu módulo :)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
No AppComponent
será construído um FormGroup
utilizando o FormBuilder
. Nosso FormGroup
(meuFormulario
) terá dois campos obrigatórios:
dataInicial
cujo o valor inicial será'2021-01-01'
-
dataFinal
cujo o valor inicial será'2021-01-15'
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
meuFormulario: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.meuFormulario = this.formBuilder.group({
// Para tornar o campo obrigatório devemos
// adicionar o Validators.required:
dataInicial: ['2021-01-01', Validators.required],
dataFinal: ['2021-01-15', Validators.required],
});
}
salvarFormulario() {
console.log('Salvo!!!');
}
}
Para vincular o FormGroup
(meuFormulario
) ao formulário no template vamos utilizar o [formGroup]
e depois utilizar o formControlName
para vincular os campos (form controls) do meuFormulario
aos <input>
do template, veja abaixo:
<!-- app.component.html -->
<!-- ... (código ocultado) ... -->
<form [formGroup]="meuFormulario" (ngSubmit)="salvarFormulario()">
<div>
<label for="dataInicial">Data Inicial</label>
<input type="text" id="dataInicial"
placeholder="yyyy-MM-dd"
formControlName="dataInicial">
</div>
<div *ngIf="meuFormulario.controls.dataInicial.errors?.required">
Por favor informe a data inicial
</div>
<div>
<label for="dataFinal">Data Final</label>
<input type="text" id="dataFinal"
placeholder="yyyy-MM-dd"
formControlName="dataFinal">
</div>
<div *ngIf="meuFormulario.controls.dataFinal.errors?.required">
Por favor informe a data final
</div>
<div>
<!--
Veja o [disabled]: o botão será desabilitado
caso haja algum erro no meuFormulario:
-->
<button [disabled]="meuFormulario.invalid"
type="submit">Enviar</button>
</div>
</form>
<!-- ... (código ocultado) ... -->
Para quem não conhece o safe navigation operator, é a interrogação (?
) utilizada na lógica de exibição de mensagem de erro:meuFormulario.controls.dataInicial.errors?.required
Neste caso a expresssão retornaránull
casoerrors
sejanull
. Perceba que sem este operador casoerrors
sejanull
ocorrerá um erro ao tentar efetuar a leitura do camporequired
de umnull
.
Testando o código acima:
Veja na animação acima que estamos validando apenas se os dois campos estão preenchidos para então habilitar o botão Enviar. Quem cria essa obrigatoriedade do preenchimento do campo é o Validators.required
que adicionamos na configuração destes campos (form controls) no AppComponent
.
A próxima etapa é construir um custom validator para cruzar estas duas informações e verificar se o intervalo entre as datas é maior que 7 dias.
Criando um custom validator para cruzar o valor dos dois inputs
Por definição um validator (ValidatorFn
) nada mais é do que uma interface que define uma função que recebe um AbstractControl
e retorna um ValidationErrors
ou null
:
// https://angular.io/api/forms/ValidatorFn
interface ValidatorFn {
(control: AbstractControl): ValidationErrors | null
}
Se ficou um pouco confuso a explicação acima, tente pensar que para criar um custom validator precisamos criar uma função que receberá como parâmetro um AbstractControl
e devolverá null
caso a validação esteja de acordo com suas políticas ou um objeto indicando os detalhes do erro da validação. Se baseando nisso vamos criar uma variável que receba uma função respeitando esta assinatura:
// intervalo-data.validator.ts
export const intervaloDataValidator =
(formGroup: AbstractControl): ValidationErrors | null => {
return null;
};
Vejam que intervalorDataValidator
é uma função que recebe como parâmetro um AbstractControl
. Até aqui temos uma função, mas quem irá chamar essa função? quem irá injetar esse AbstractControl
?
Se repararmos bem, o FormGroup
extende a classe AbstractControl
. Então quem irá injetar este FormGroup
é o próprio Angular e quem irá chamar essa validação é o seu FormGroup
. Confuso? veja abaixo na prática como isso é simples:
Como nossa função de validação irá receber o FormGroup
(meuFormulario
), nos dará acesso ao dataInicial
e dataFinal
. Desta forma conseguimos criar a lógica para validar o intervalo de data conforme as etapas abaixo:
- Pegar os valores dos inputs:
dataInicial
edataFinal
- Converter para um objeto do tipo
Date
para facilitar o cálculo - Calcular a diferença de dias
- Se o intervalo for menor que 7 será retornado um erro
Traduzindo estas 4 etapas em código:
// intervalo-data.validator.ts
export const intervaloDataValidator =
(formGroup: AbstractControl): ValidationErrors | null => {
// Etapa 1
const valorDataInicial = formGroup.get('dataInicial').value;
const valorDataFinal = formGroup.get('dataFinal').value;
// Etapa 2
const dataInicial = new Date(valorDataInicial);
const dataFinal = new Date(valorDataFinal);
// Etapa 3
const diferencaEmDias =
(dataFinal.getTime() - dataInicial.getTime())
/ (1000 * 60 * 60 * 24);
// Etapa 4
return diferencaEmDias < 7
? {
intervaloData: {
atual: diferencaEmDias,
min: 7
}
}
: null;
};
Quando nossa função retornar null
, significa que a informação está válida, no nosso caso significa que o intervalo é maior que 7, caso contrário será retornado um objeto com os detalhes do erro no seguinte formato:
{
intervaloData: {
atual: 999,
min: 7
}
}
Vinculando o custom validator ao formGroup
Para vincular o intervaloDataValidator
ao meuFormulario
basta adicionarmos um segundo argumento em FormBuilder.group
:
// app.component.ts
import { intervaloDataValidator } from './validators/intervalo-data.validator';
// ... (código ocultado) ...
ngOnInit() {
this.meuFormulario = this.formBuilder.group({
dataInicial: ['2021-01-01', Validators.required],
dataFinal: ['2021-01-15', Validators.required],
}, {
// *******************************
// Registrando o custom validator:
// *******************************
validators: [intervaloDataValidator]
});
}
// ... (código ocultado) ...
Agora que o custom validator está vinculado, vamos editar o template de modo a exibir a mensagem de validação de intervalo:
<!-- app.component.html -->
<!-- ... (código ocultado) ... -->
<form [formGroup]="meuFormulario" (ngSubmit)="salvarFormulario()">
<div>
<label for="dataInicial">Data Inicial</label>
<input type="text" id="dataInicial"
placeholder="yyyy-MM-dd"
formControlName="dataInicial">
</div>
<div *ngIf="meuFormulario.controls.dataInicial.errors?.required">
Por favor informe a data inicial
</div>
<div>
<label for="dataFinal">Data Final</label>
<input type="text" id="dataFinal"
placeholder="yyyy-MM-dd"
formControlName="dataFinal">
</div>
<div *ngIf="meuFormulario.controls.dataFinal.errors?.required">
Por favor informe a data final
</div>
<!-- ****************************************************** -->
<!-- Inclusão da mensagem de validação do intervalo de data -->
<!-- ****************************************************** -->
<div *ngIf="meuFormulario.errors?.intervaloData">
O intervalo deve ser
maior que {{meuFormulario.errors.intervaloData.min}} dias.
Valor atual: {{meuFormulario.errors.intervaloData.atual}}
</div>
<div>
<!--
Veja o [disabled]: o botão será desabilitado
caso haja algum erro no meuFormulario:
-->
<button [disabled]="meuFormulario.invalid"
type="submir">Enviar</button>
</div>
</form>
<!-- ... (código ocultado) ... -->
Resultado da validação do intervalo de data:
Melhorando o código
Com o objetivo de reaproveitar o intervaloDataValidator
vamos parametrizar o nome dos campos (dataInicial
e dataFinal
) utilizados dentro do custom validator.
Para isto, ao invés de utilizarmos uma constante que representava uma função, agora criamos uma função que recebe como parâmetro o nome dos campos (data inicial e data final) e retorna uma outra função:
// intervalo-data.validator.ts
// Troca do const por function:
export function intervaloDataValidator(nomeCampo1: string, nomeCampo2: string) {
// Agora retornamos uma função como resultado:
return (formGroup: AbstractControl): ValidationErrors | null => {
// Etapa 1
const valorDataInicial = formGroup.get(nomeCampo1).value;
const valorDataFinal = formGroup.get(nomeCampo2).value;
// ... (código ocultado) ...
};
}
No AppComponent
a alteração é bem simples:
// app.component.ts
// ... (código ocultado) ...
ngOnInit() {
this.meuFormulario = this.formBuilder.group({
dataInicial: ['2021-01-01', Validators.required],
dataFinal: ['2021-01-15', Validators.required],
}, {
validators: [intervaloDataValidator('dataInicial', 'dataFinal')]
});
}
// ... (código ocultado) ...
Com esta pequena alteração conseguimos reaproveitar este custom validator em outros lugares. Para melhorar ainda mais, poderíamos parametrizar o intervalo mínimo que atualmente é de 7 dias:
// intervalo-data.validator.ts
export function intervaloDataValidator(nomeCampo1: string,
nomeCampo2: string,
intervalorMinimo: number = 7) {
return (formGroup: AbstractControl): ValidationErrors | null => {
// Etapa 1
const valorDataInicial = formGroup.get(nomeCampo1).value;
const valorDataFinal = formGroup.get(nomeCampo2).value;
// Etapa 2
const dataInicial = new Date(valorDataInicial);
const dataFinal = new Date(valorDataFinal);
// Etapa 3
const diferencaEmDias =
(dataFinal.getTime() - dataInicial.getTime())
/ (1000 * 60 * 60 * 24);
// Etapa 4
return diferencaEmDias < intervalorMinimo
? {
intervaloData: {
atual: diferencaEmDias,
min: intervalorMinimo
}
}
: null;
};
}
// app.component.ts
// ... (código ocultado) ...
ngOnInit() {
this.meuFormulario = this.formBuilder.group({
dataInicial: ['2021-01-01', Validators.required],
dataFinal: ['2021-01-15', Validators.required],
}, {
validators: [intervaloDataValidator('dataInicial', 'dataFinal', 7)]
});
}
// ... (código ocultado) ...
Considerações
No processo de validação nós ignoramos o formato da data que o usuário digita para focarmos apenas em como construir um custom validator para validar dois campos relacionados.
De um modo geral essa lógica poderá ser empregada em outras situações como por exemplo, a confirmação de senha ou e-mail onde o usuário digita duas vezes as mesma informação para garantir que não há erros durante a digitação.
Exemplo da implementação deste artigo: https://stackblitz.com/edit/validacao-de-intervalo-de-data-utilizando-custom-validator-do-r?file=src/app/app.component.html
Links interessantes para leitura: