Como aplicar uma máscara de telefone, CPF, IP e data com ngx-mask

Quando falamos em formulários web, a entrada de dados deve sempre ser tratada. Todo e qualquer dado fornecido por um usuário deve ser sanitizado, tratado.

Desde números de telefone até datas e códigos de cartão de crédito, garantir a precisão e consistência dessas informações deve ser uma das prioridades do projeto. As máscaras em inputs surgem como uma solução elegante para ajudar nesse problema.

Em termos simples, uma máscara é como uma camada de orientação para o seu formulário. Ela define o formato esperado para a entrada de dados, guiando o usuário durante o preenchimento. Isso não apenas facilita a vida do usuário, mas também ajuda a prevenir erros comuns.

Exemplo de uma máscara para CPF e outra para telefone

Utilizando o ngx-mask em um projeto Angular

Em projetos Angular, podemos recorrer a uma biblioteca chamada ngx-mask. Este pacote simplifica a aplicação de máscaras em inputs. Ele é particularmente útil quando se trata de formatar e validar dados de entrada, como números de telefone, datas, CPFs, entre outros, em formulários web.

Para demonstrar o uso da ngx-mask vou criar um projeto do zero utilizando as seguintes versões:

$ ng version

Angular CLI: 16.2.3
Node: 18.18.2
Package Manager: npm 9.8.1
OS: darwin arm64

No comando abaixo utilizei o --skip-git para não criar um repositório GIT local e usei o --minimal para não criar uma estrutura para testes unitários, já que o projeto será utilizado apenas para estudo:

$ ng new --skip-git --minimal utilizando-ngx-mask
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
💡
Se quiser saber mais sobre as opções do ng new acesse este link ou digite ng new help no seu terminal.

Como o exemplo será construído utilizando Angular 16, a versão do ngx-mask deve ser a 16 conforme indicado na documentação oficial:

$ npm install --save ngx-mask@16.4.1

Com o único objetivo de melhorar o layout, também adicionei o bootstrap na lista de dependências para utilizar algumas classes CSS:

$ npm i bootstrap
💡
Se quiser saber mais sobre como utilizar o Bootstrap em seu projeto Angular: https://consolelog.com.br/configurar-bootstrap-angular-dropdown-tooltip/

Configurando o projeto para usar ngx-mask

Com o projeto já criado e as dependências instaladas, para começar a utilizar o ngx-mask, importe o NgxMaskDirective e registre o provideNgxMask() no arquivo app.module.ts conforme a seguir:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { ReactiveFormsModule } from '@angular/forms';
import { NgxMaskDirective, provideNgxMask } from 'ngx-mask';
import { AppComponent } from './app.component';

@NgModule({
 declarations: [AppComponent],
 imports: [
  BrowserModule,
  NgxMaskDirective,

  // Também incluí o módulo abaixo
  // porque vamos trabalhar com o FormGroup
  // ao longo do texto
  ReactiveFormsModule,
 ],
 providers: [provideNgxMask({ /* opções de cfg */ })],
 bootstrap: [AppComponent],
})
export class AppModule {}

app.module.ts

Para conferir se a configuração está correta, alterei o AppComponent conforme a seguir:

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  templateUrl: "app.component.html",
})
export class AppComponent {}

app.component.ts

<div class="container">
 <div class="row">
  <div class="col">
   <form>
    <h1>Formulário de cadastro</h1>
    <div class="mb-3">
     <label for="input-cpf" class="form-label">
      CPF
     </label>
     <input
      autofocus
      class="form-control"
      id="input-cpf"
      inputmode="numeric"
      mask="000.000.000-00"
      placeholder="CPF"
      type="text" />
    </div>
    <div class="mb-3">
     <label for="input-telefone" class="form-label">
      Telefone
     </label>
     <input
      class="form-control"
      id="input-telefone"
      mask="(00) 0 0000 0000"
      prefix="+55 "
      type="tel" />
    </div>
    <div class="mb-3">
     <label for="input-ip" class="form-label">
      IP
     </label>
     <input
      class="form-control"
      id="input-ip"
      inputmode="numeric"
      mask="IP" />
    </div>
    <div class="mb-3">
     <label for="input-data" class="form-label">
      Data
     </label>
     <input
      class="form-control"
      id="input-data"
      inputmode="numeric"
      mask="d0/M0/0000" />
    </div>
   </form>
  </div>
 </div>
</div>

app.component.html

Resultado:

Resultado

O básico do ngx-mask

Uma vez configurado o ngx-mask, podemos utilizar o atributo mask nos inputs para configurar a máscara. Por exemplo:

(00) 0 0000 0009
   │  │    │ │ │
   │  │    │ └►└► patterns
   │  │    │
   └─►└───►└────► special characters

O (, ) e espaço são special characters e o 0 e 9 são patterns. Isto significa que quando o usuário digitar algo, os special characters serão mantidos no input e o 0 e 9 substituídos pelos números que o usuário digitar, sendo que 9 é um número opcional.

Segundo a documentação, por padrão a lista de special characters é a seguinte:

- / ( ) . : space + , @ [ ] " ' 

Novamente, os special characters são valores fixos no seu input, são valores que o usuário não pode alterar, por exemplo, o preenchimento da máscara [0] (0)-0 pode ser [3] (4)-5.

Lista dos patterns:

  • 0: somente número
  • 9: somente número, porém, opcional
  • A: letra (maiúscula ou minúscula) ou número
  • S: somente letra (minúscula ou maiúscula)
  • U: somente letra maiúscula
  • L: somente letra minúscula

Além dessa lista de patterns, há outras opções como por exemplo:

  • IP - aplica uma máscara e valida o valor digitado
  • percent - utilizado para porcentagem
  • CPF_CNPJ - aplica uma máscara para CPF ou CNPJ
  • Hh:m0:s0 - máscara para hora (24h), minuto e segundo
  • d0/M0/0000 - máscara para data, mês e ano

A lista completa pode ser acessada neste link.

Integração com ReactiveFormsModule

Por padrão do ngx-mask, caso algum input não esteja preenchido com a máscara corretamente, o FormControl associado ao input é invalidado. Essa opção pode ser controlada através do atributo [validation]="true | false" (sendo o valor padrão true).

No exemplo a seguir, criei um formulário que utiliza a máscara em todos os inputs. Somente no primeiro input, CPF, deixei o [validation]="false" para demonstrar esse comportamento. Observe o código e o resultado a seguir:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
 selector: 'app-root',
 templateUrl: 'app.component.html',
})
export class AppComponent {
 form: FormGroup;

 get cpf() {
  return this.form.get('cpf')!;
 }

 get telefone() {
  return this.form.get('telefone')!;
 }

 get ip() {
  return this.form.get('ip')!;
 }

 get data() {
  return this.form.get('data')!;
 }

 constructor(formBuilder: FormBuilder) {
  this.form = formBuilder.nonNullable.group({
   cpf: [''],
   telefone: [''],
   ip: [''],
   data: [''],
  });
 }
}

app.component.ts

<div class="container">
 <div class="row">
  <div class="col">
   <form [formGroup]="form">
    <h1>Formulário de cadastro</h1>
    <div class="mb-3">
     <label for="input-cpf" class="form-label">CPF</label>
     <input
      [class.is-invalid]="cpf.touched && cpf.invalid"
      [validation]="false"
      autofocus
      class="form-control"
      formControlName="cpf"
      id="input-cpf"
      mask="000.000.000-00"
      placeholder="CPF"
      type="text" />
    </div>
    <div class="mb-3">
     <label for="input-telefone" class="form-label">
      Telefone
     </label>
     <input
      [class.is-invalid]="telefone.touched && telefone.invalid"
      class="form-control"
      formControlName="telefone"
      id="input-telefone"
      mask="(00) 0 0000 0000"
      prefix="+55 "
      type="tel" />
    </div>
    <div class="mb-3">
     <label for="input-ip" class="form-label">IP</label>
     <input
      [class.is-invalid]="ip.touched && ip.invalid"
      class="form-control"
      formControlName="ip"
      id="input-ip"
      inputmode="numeric"
      mask="IP" />
    </div>
    <div class="mb-3">
     <label for="input-data" class="form-label">Data</label>
     <input
      [class.is-invalid]="data.touched && data.invalid"
      class="form-control"
      formControlName="data"
      id="input-data"
      inputmode="numeric"
      mask="d0/M0/0000" />
    </div>
    <button
     [disabled]="form.invalid"
     type="submit"
     class="btn btn-primary">
     Enviar
    </button>
   </form>
   <div class="border border-primary m-4">
    <pre class="p-4"><code>Valor do formulário (form.value):
 
 {{form.value | json }}</code></pre>
   </div>
  </div>
 </div>
</div>

app.component.html

💡
Observe que o trecho {{ form.value | json }} imprime o valor do formulário na tela.
Demonstrando o uso do [validation]="false"

Como resultado, observe que não preenchemos completamente os campos CPF nem Telefone; no entanto, apenas o campo Telefone ficou inválido. Isso ocorre porque configuramos no ngx-mask para que o FormControl vinculado ao CPF não seja considerado inválido caso a máscara não tenha sido totalmente preenchida.

Obtendo o valor com a máscara

Quando aplicamos a máscara 000.000.000-00 a um input e o vinculamos a um FormControl, após o usuário digitar o valor no input, ele visualizará na tela algo como 123.123.123-12. Entretanto, o valor obtido no FormControl.value será 12312312312, ou seja, por padrão, os caracteres especiais (special characters) não são incluídos no FormControl.value.

Em alguns cenários, como por exemplo, IP e Data, é interessante ter o valor completo, ou seja, com os pontos e barras. A boa notícia é que a ngx-mask já oferece essa opção através do atributo [dropSpecialCharacters]="true | false" (default: true).

Para demonstrar, utilizei o código HTML mencionado um pouco mais acima e adicionei o [dropSpecialCharacters]="false" no campo IP."

<div class="mb-3">
 <label for="input-ip" class="form-label">IP</label>
 <input
  [class.is-invalid]="ip.touched && ip.invalid"
  [dropSpecialCharacters]="false"
  class="form-control"
  formControlName="ip"
  id="input-ip"
  inputmode="numeric"
  mask="IP" />
</div>

trecho do app.component.html

Demonstrando o uso do [dropSpecialCharacters]=false

Veja na parte inferior da imagem que o valor do cpf contém apenas números e o valor do ip tem os números e os pontos.

Formulário com validação do CPF, telefone, IP e data com máscaras

Por fim, adicionei algumas validações no formulário, sendo algumas nativas e outras customizadas (ver arquivo app.validators.ts abaixo). Além disso, fiz pequenas modificações no HTML para exibir de forma amigável algumas mensagens de erro. Veja a seguir o código fonte:

import {
 AbstractControl,
 ValidationErrors,
 ValidatorFn,
} from '@angular/forms';

export class AppValidators {
 static isCPF(
  control: AbstractControl
 ): ValidationErrors | null {
  // Remover caracteres não numéricos
  const cpf = control.value.replace(/\D/g, '');

  // Verificar se o CPF tem 11 dígitos
  if (cpf.length !== 11) {
   return { isCPF: true };
  }

  // Verificar se todos os dígitos são
  // iguais (evitar CPFs como 111.111.111-11)
  if (/^(\d)\1+$/.test(cpf)) {
   return { isCPF: true };
  }

  // Calcular os dígitos verificadores
  let soma = 0;
  for (let i = 0; i < 9; i++) {
   soma += parseInt(cpf.charAt(i)) * (10 - i);
  }

  let resto = 11 - (soma % 11);
  if (resto === 10 || resto === 11) {
   resto = 0;
  }

  if (resto !== parseInt(cpf.charAt(9))) {
   return { isCPF: true };
  }

  soma = 0;
  for (let i = 0; i < 10; i++) {
   soma += parseInt(cpf.charAt(i)) * (11 - i);
  }

  resto = 11 - (soma % 11);
  if (resto === 10 || resto === 11) {
   resto = 0;
  }

  if (resto !== parseInt(cpf.charAt(10))) {
   return { isCPF: true };
  }

  return null;
 }

 static minDate(min: Date): ValidatorFn {
  return (
   control: AbstractControl
  ): ValidationErrors | null => {
   const [dd, mm, yyyy] = control.value
    .split('/')
    .map((a: string) => a || '0')
    .map(Number);

   const date = new Date(yyyy, mm - 1, dd);
   if (date.getTime() > min.getTime()) {
    return null;
   }

   return { minDate: min };
  };
 }
}

app.validators.ts

💡
O arquivo acima, app.validators.ts, demonstra como podemos criar validações customizadas. Estas função podem ser usadas na configuração do FormGroup. Inclusive isso já foi tema por aqui: https://consolelog.com.br/como-validar-um-intervalo-de-data-utilizando-custom-validator-reactiveforms-angular2/
import { Component } from '@angular/core';
import {
 FormBuilder,
 FormGroup,
 Validators,
} from '@angular/forms';
import { AppValidators } from './app.validators';

@Component({
 selector: 'app-root',
 templateUrl: 'app.component.html',
})
export class AppComponent {
 form: FormGroup;

 get cpf() {
  return this.form.get('cpf')!;
 }

 get telefone() {
  return this.form.get('telefone')!;
 }

 get ip() {
  return this.form.get('ip')!;
 }

 get data() {
  return this.form.get('data')!;
 }

 constructor(formBuilder: FormBuilder) {
  this.form = formBuilder.nonNullable.group({
   cpf: ['', [Validators.required, AppValidators.isCPF]],
   telefone: ['', [Validators.required]],
   ip: ['', [Validators.required]],
   data: [
    '',
    [
     Validators.required,
     AppValidators.minDate(
      new Date('2023-11-01T00:00:00.000-0300')
     ),
    ],
   ],
  });
 }
}

app.component.ts

<div class="container">
 <div class="row">
  <div class="col">
   <form [formGroup]="form">
    <h1>Formulário de cadastro</h1>
    <div class="mb-3">
     <label for="input-cpf" class="form-label">CPF</label>
     <input
      [class.is-invalid]="cpf.touched && cpf.invalid"
      autofocus
      class="form-control"
      formControlName="cpf"
      id="input-cpf"
      mask="000.000.000-00"
      placeholder="CPF"
      type="text" />
     <div
      *ngIf="cpf.errors?.['isCPF']"
      class="invalid-feedback">
      Informe um CPF válido
     </div>
     <div
      *ngIf="cpf.errors?.['required']"
      class="invalid-feedback">
      Por favor, informe um CPF.
     </div>
    </div>
    <div class="mb-3">
     <label for="input-telefone" class="form-label">
      Telefone
     </label>
     <input
      [class.is-invalid]="
       telefone.touched && telefone.invalid
      "
      class="form-control"
      formControlName="telefone"
      id="input-telefone"
      mask="(00) 0 0000 0000"
      prefix="+55 "
      type="tel" />
     <div
      *ngIf="telefone.errors?.['required']"
      class="invalid-feedback">
      Por favor, informe um número de telefone.
     </div>
    </div>
    <div class="mb-3">
     <label for="input-ip" class="form-label">IP</label>
     <input
      [class.is-invalid]="ip.touched && ip.invalid"
      [dropSpecialCharacters]="false"
      class="form-control"
      formControlName="ip"
      id="input-ip"
      inputmode="numeric"
      mask="IP" />
     <div
      *ngIf="ip.errors?.['required']"
      class="invalid-feedback">
      Por favor, informe um IP.
     </div>
    </div>
    <div class="mb-3">
     <label for="input-data" class="form-label">Data</label>
     <input
      [class.is-invalid]="data.touched && data.invalid"
      [dropSpecialCharacters]="false"
      class="form-control"
      formControlName="data"
      id="input-data"
      inputmode="numeric"
      mask="d0/M0/0000" />
     <div
      *ngIf="data.errors?.['required']"
      class="invalid-feedback">
      Por favor, informe uma data
     </div>
     <div
      *ngIf="data.errors?.['minDate']"
      class="invalid-feedback">
      Por favor, informe uma data maior que
      {{data.errors?.['minDate'] | date : 'dd/MM/yyyy'}}
     </div>
    </div>
    <button
     [disabled]="form.invalid"
     type="submit"
     class="btn btn-primary">
     Enviar
    </button>
   </form>
   <div class="border border-primary m-4">
    <pre class="p-4"><code>Valor do formulário (form.value):
 
 {{form.value | json }}</code></pre>
   </div>
  </div>
 </div>
</div>

app.component.html

Resultado final:

Resultado final

Um último ponto que vale comentar é que, por padrão, as validações são aplicadas no evento onBlur, ou seja, quando o usuário clica no input e depois sai. Se quiser modificar este comportamento e validar conforme o usuário digita, utilize a opção updateOn na configuração do FormControl.

Considerações

Ao incorporar máscaras nos inputs, proporcionamos não apenas uma diretriz visual para os usuários, mas também estabelecemos um mecanismo que promove a consistência dos dados. Essa abordagem não só simplifica o processo de preenchimento, mas também desempenha um papel crucial na prevenção de erros comuns.

Além disso, a ngx-mask, integrada ao Angular, destaca-se por sua flexibilidade e facilidade de implementação. Seja lidando com formatos específicos de data, exigências precisas para números de telefone ou padrões específicos para códigos, a biblioteca oferece uma gama de opções configuráveis.

Links para documentação: