Como configurar e utilizar o Angular Material
Para desenvolver um sistema web, é essencial que a interface gráfica conte com componentes visuais que viabilizem a interação do usuário, tais como botões, textos, cards, imagens, entre outros elementos. Para não termos que "reinventar a roda", podemos recorrer a bibliotecas de componentes visuais, como o Angular Material ou o Ng Prime, por exemplo.
Neste artigo, abordaremos o processo de configuração do Angular Material ao mesmo tempo em que criaremos um pequeno projeto para ilustrar o potencial que essa biblioteca oferece.
O que é o Angular Material?
Angular Material é um conjunto de componentes prontos para uso que segue as diretrizes do Material Design, uma abordagem de design criada pelo Google. O Material Design se concentra na usabilidade, na consistência visual e na criação de interfaces modernas e agradáveis. O Angular Material facilita muito a criação de aplicações com essa estética, proporcionando uma experiência de usuário mais rica e envolvente.
Criando o projeto Angular do zero
Para criar o projeto utilizei as seguintes versões:
Angular CLI: 16.2.3
Node: 18.18.0
Package Manager: npm 9.8.1
Utilizando o Angular CLI para criar o projeto:
ng new exemplo-angular-material
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS
Testando o projeto recém-criado:
cd exemplo-angular-material
npm start
Configurando o Angular Material
Para efetuar a configuração do Angular Material basta executar o ng add @angular/material
conforme abaixo:
ng add @angular/material
ℹ Using package manager: npm
✔ Found compatible package version: @angular/material@16.2.5.
✔ Package information loaded.
The package @angular/material@16.2.5 will be installed and executed.
Would you like to proceed? Yes
✔ Packages successfully installed.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Include the Angular animations module? Include and enable animations
UPDATE package.json (1121 bytes)
✔ Packages installed successfully.
UPDATE src/app/app.module.ts (502 bytes)
UPDATE angular.json (3137 bytes)
UPDATE src/index.html (590 bytes)
UPDATE src/styles.scss (181 bytes)
Para testarmos a instalação, vou incluir somente um botão no app.component.html
e utilizar a diretiva mat-flat-button
para criar um botão do Angular Material:
<!--
Removi o conteúdo original do app.component.html
Abaixo adicionei apenas a diretiva
mat-flat-button
-->
<div style="padding: 24px">
<button mat-flat-button color="primary">Olá</button>
</div>
Sempre que for utilizar um componente do Angular Material, é necessário importar seu respectivo módulo no módulo onde seu componente foi declarado, como fizemos no código abaixo. Para saber qual o nome do módulo do componente (do Angular Material), basta consultar a documentação oficial neste link.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatButtonModule, // <- Módulo p/usar o botão do Material
],
bootstrap: [AppComponent],
})
export class AppModule {}
Executando o projeto novamente (npm start
) podemos ver que é renderizado na tela um botão exatamente como na documentação do Angular Material:
Exemplo prático - utilizando os componentes do Angular Material
Agora que o Angular Material está devidamente configurado, iremos avançar para a criação de uma tela que inclui um formulário de consulta e uma tabela de resultados. Quando você clicar em uma linha da tabela, um modal será aberto para exibir os detalhes.
Como fonte de dados, utilizaremos uma API pública fornecida pelo IBGE para apresentar informações sobre a frequência de uso de nomes ao longo das décadas.
O resultado final ficará da seguinte forma:
Criando a integração com a API
Inicialmente, vamos criar uma interface que representará o modelo de dados obtidos pela API, bem como um serviço para realizar consultas. Vou gerar esses arquivos usando o CLI, mas caso prefira, você também pode criá-los manualmente.
ng generate interface frequencia-nome model
ng generate service ibge
Os comandos acima irão criar dois arquivos. Cada um terá o seguinte conteúdo:
export interface FrequenciaNome {
nome: string;
res: Array<FrequenciaNomePeriodo>;
}
export interface FrequenciaNomePeriodo {
periodo: string;
frequencia: number;
}
import {
HttpClient,
HttpErrorResponse,
HttpStatusCode,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, catchError, map, throwError } from 'rxjs';
import { FrequenciaNome } from './frequencia-nome.model';
@Injectable({
providedIn: 'root',
})
export class IBGEService {
urlBase = 'https://servicodados.ibge.gov.br';
constructor(private httpClient: HttpClient) {}
get(nome: string): Observable<FrequenciaNome> {
const url = this.urlBase + `/api/v2/censos/nomes/${nome}`;
// A API retorna um array, por isso o "FrequenciaNome[]"
// logo abaixo
return this.httpClient.get<FrequenciaNome[]>(url).pipe(
map((response) => {
// A API não retorna 404 quando não encontra o recurso.
// Então quando a API nào retornada nada, emitimos
// manualmente um 404:
if (response.length === 0) {
throw new HttpErrorResponse({
status: HttpStatusCode.NotFound,
statusText: `Não foram encontrados registros
para "${nome}"`,
});
}
const saida = response[0];
// Tratamento para melhorar a formatação:
saida.res.forEach((item) => {
const [de, ate] = item.periodo
// Retira caracteres diferentes de números e vírgula
.replace(/[^\d,]+/g, '')
// Separa o resultado por vírgula
.split(',');
item.periodo = ate ? `${de} - ${ate}` : de;
});
return saida;
}),
catchError((error) => {
let mensagem = '';
if (error instanceof ErrorEvent) {
mensagem = error.message;
} else if (error instanceof HttpErrorResponse) {
mensagem = error.statusText;
} else {
mensagem = 'Ocorreu um erro';
}
return throwError(() => mensagem);
}),
);
}
}
Como estamos utilizando o HttpClient
, precisamos importar o HttpClientModule
. Neste caso importaremos o módulo do arquivo app.module.ts
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
HttpClientModule,
// * ***************************************************
// * Componentes Angular:
// * ***************************************************
MatButtonModule,
],
bootstrap: [AppComponent],
})
export class AppModule {}
Criando a interface do usuário
Agora, nossa atenção se volta para a interface do usuário, isto é, na criação da tela na qual os usuários poderão efetuar consultas e visualizar os resultados. Para isso, iremos utilizar dois componentes:
- AppComponent: Este componente servirá como a página principal para consulta e exibição dos dados.
- ModalFrequenciaNome: Este componente será responsável pelo conteúdo do modal que aparecerá quando um usuário clicar em uma linha na tabela de resultados.
Construindo o modal - ModalFrequenciaNome
Para criar o componente:
ng generate component ModalFrequenciaNome
O conteúdo do componente ficará da seguinte forma:
<h1 mat-dialog-title>{{ nome }}</h1>
<div mat-dialog-content>
<div>
<p>
A frequência do nome
<strong>{{ nome }}</strong>
é {{ dados.frequencia | number }} entre o período de
<strong>{{ dados.periodo }}</strong>
</p>
</div>
</div>
<div mat-dialog-actions>
<button mat-button cdkFocusInitial mat-dialog-close>Fechar</button>
</div>
Observação: os módulos das diretivas do Angular Material que utilizei acima serão importadas no AppModule
. Ao longo deste texto vou mostrar.
import { Component, Input } from '@angular/core';
import { FrequenciaNomePeriodo } from '../frequencia-nome.model';
@Component({
selector: 'app-modal-frequencia-nome',
templateUrl: './modal-frequencia-nome.component.html',
styleUrls: ['./modal-frequencia-nome.component.scss'],
})
export class ModalFrequenciaNomeComponent {
@Input() nome: string = '';
@Input() dados: FrequenciaNomePeriodo = {
periodo: '',
frequencia: 0,
};
}
Agora que o código do modal está pronto, vamos construir a tela com os componentes do Angular Material.
Construindo o formulário e a tabela com o resultado da consulta
Sendo bem direto, a construção da tela ficou da seguinte forma:
import { Component, ElementRef, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { finalize } from 'rxjs';
import { FrequenciaNome, FrequenciaNomePeriodo } from './frequencia-nome.model';
import { IBGEService } from './ibge.service';
import { ModalFrequenciaNomeComponent } from './modal-frequencia-nome/modal-frequencia-nome.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
dadosFrequenciaNome: FrequenciaNome | null = null;
mostrarCarregando = false;
colunasDaTabela: string[] = ['periodo', 'frequencia'];
formularioPesquisa: FormGroup = new FormGroup({
nome: new FormControl<string>('', [
Validators.required,
Validators.minLength(3),
]),
});
@ViewChild('inputNome')
inputNome: ElementRef<HTMLInputElement> | null = null;
get nomeFormularioPesquisa() {
return this.formularioPesquisa.get('nome')!;
}
constructor(
private dialog: MatDialog,
private ibgeService: IBGEService,
private snackBar: MatSnackBar
) {}
pesquisar() {
if (this.formularioPesquisa.invalid) {
return;
}
this.mostrarCarregando = true;
const { nome } = this.formularioPesquisa.value;
this.ibgeService
.get(nome)
// Quando o Observable dá erro ou retorna seu valor, o
// "finalize" é executado e escondemos o loading
.pipe(finalize(() => (this.mostrarCarregando = false)))
.subscribe({
next: (dados) => (this.dadosFrequenciaNome = dados),
error: (error) => {
this.reiniciarFormulario();
this.snackBar.open(error);
},
});
}
reiniciarFormulario() {
this.dadosFrequenciaNome = null;
this.formularioPesquisa.reset();
this.inputNome?.nativeElement.focus();
}
mostrarModal(nome: string, dados: FrequenciaNomePeriodo) {
const refModal = this.dialog.open(ModalFrequenciaNomeComponent);
const modal = refModal.componentInstance;
modal.nome = nome;
modal.dados = dados;
}
}
No template:
<mat-toolbar color="primary">
<span>Pesquisa de Frequência de Nomes</span>
</mat-toolbar>
<div class="container">
<form [formGroup]="formularioPesquisa" (ngSubmit)="pesquisar()">
<div>
<p>
Utilize o formulário abaixo para pesquisar a frequência de um nome entre
as décadas.
</p>
<p>
Os dados são obtidos da API do IBGE:
<a href="https://servicodados.ibge.gov.br" target="_blank">
https://servicodados.ibge.gov.br
</a>
</p>
</div>
<div>
<mat-form-field>
<mat-label>Nome</mat-label>
<input
#inputNome
matInput
autofocus
autocomplete="off"
formControlName="nome"
placeholder="Digite um nome..."
/>
<mat-error *ngIf="nomeFormularioPesquisa.hasError('required')">
Informe um nome para efetuar a busca
</mat-error>
<mat-error *ngIf="nomeFormularioPesquisa.hasError('minlength')">
Informe ao menos <strong>3</strong> caracteres
</mat-error>
</mat-form-field>
</div>
<div class="mt-1">
<button
mat-flat-button
[disabled]="formularioPesquisa.invalid"
color="primary"
type="submit"
>
Pesquisar
</button>
<button mat-flat-button (click)="reiniciarFormulario()" class="ml-2">
reset
</button>
</div>
</form>
<div *ngIf="dadosFrequenciaNome">
<div class="mt-1 mb-1">
Resultado da pesquisa pelo nome:
<strong>{{ dadosFrequenciaNome.nome }}</strong>
</div>
<table
mat-table
[dataSource]="dadosFrequenciaNome.res"
class="mat-elevation-z8"
>
<!-- Período -->
<ng-container matColumnDef="periodo">
<th mat-header-cell *matHeaderCellDef>Período</th>
<td mat-cell *matCellDef="let element">
{{ element.periodo }}
</td>
</ng-container>
<!-- Frequência -->
<ng-container matColumnDef="frequencia">
<th mat-header-cell *matHeaderCellDef>Frequência</th>
<td mat-cell *matCellDef="let element; let i = index">
<ng-container
*ngIf="i > 0"
[ngTemplateOutlet]="
element.frequencia > dadosFrequenciaNome.res[i - 1].frequencia
? setaAumento
: setaReducao
"
></ng-container>
{{ element.frequencia | number }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="colunasDaTabela"></tr>
<tr
mat-row
*matRowDef="let row; columns: colunasDaTabela"
(click)="mostrarModal(dadosFrequenciaNome.nome, row)"
></tr>
</table>
</div>
</div>
<div *ngIf="mostrarCarregando" class="loading">
<mat-spinner></mat-spinner>
</div>
<ng-template #setaAumento>
<mat-icon
color="primary"
aria-hidden="false"
aria-label="Aumento"
fontIcon="trending_up"
></mat-icon>
</ng-template>
<ng-template #setaReducao>
<mat-icon
color="warn"
aria-hidden="false"
aria-label="Redução"
fontIcon="trending_down"
></mat-icon>
</ng-template>
Por último, é necessário importar alguns módulos e fazer uma pequena configuração de internacionalização no AppModule
. Além disso, importei o ReactiveFormsModule
, pois estaremos usando um FormGroup
e um FormControl
na tela que será desenvolvida a seguir:
import { registerLocaleData } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import localePT from '@angular/common/locales/pt';
import { LOCALE_ID, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatToolbarModule } from '@angular/material/toolbar';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ModalFrequenciaNomeComponent } from './modal-frequencia-nome/modal-frequencia-nome.component';
// * ***************************************************
// * Registramos o esquema de formatação para o locale
// * ***************************************************
registerLocaleData(localePT);
@NgModule({
bootstrap: [AppComponent],
declarations: [AppComponent, ModalFrequenciaNomeComponent],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
HttpClientModule,
ReactiveFormsModule,
// * ***************************************************
// * Componentes Angular:
// * ***************************************************
MatButtonModule,
MatCardModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTableModule,
MatToolbarModule,
],
providers: [
// * ***************************************************
// * Ajustamos o locale padrão da aplicação.
// * ***************************************************
{
provide: LOCALE_ID,
useValue: 'pt-BR',
},
],
})
export class AppModule {}
Resultado até o momento:
Um pouco de CSS para ajustar o visual:
/* You can add global styles to this file, and also import other style files */
html,
body {
height: 100%;
background: whitesmoke;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.container {
display: grid;
margin: auto;
gap: 24px;
width: 1200px;
}
mat-form-field {
width: 100%;
}
// *****************************************************
// * Gera algumas classes CSS para ajudar no layout
// * Por exemplo: ml-1, ml-2, ml-3 e ml-4
// *****************************************************
@for $i from 1 through 4 {
.ml-#{$i} {
margin-left: 8px * $i;
}
.mr-#{$i} {
margin-right: 8px * $i;
}
.mt-#{$i} {
margin-top: 8px * $i;
}
.mb-#{$i} {
margin-bottom: 8px * $i;
}
.m-#{$i} {
margin: 8px * $i;
}
}
Resultado final:
Considerações
A configuração para usar o Angular Material é rápida e simples. Sua variedade de componentes prontos para uso pode agilizar consideravelmente o desenvolvimento do seu projeto, poupando-o de criar componentes como tabelas, botões, entre outros.
Embora eu não tenha explorado minuciosamente cada componente nem adentrado em detalhes do projeto de exemplo, abaixo você encontrará alguns links onde abordo em profundidade diversos tópicos relacionados à construção deste projeto:
- Validação de formulários utilizando Reactive Forms
- Como fazer requisições AJAX com Angular 2+
- Construindo componentes mais flexíveis com ngTemplateOutlet - Angular 2+
- Angular Material
Link do projeto utilizado neste texto: