Como configurar e utilizar o Angular Material

Aprenda a configurar e aproveitar ao máximo o Angular Material. Este guia abrange desde o processo de configuração inicial até a exploração dos componentes e recursos disponíveis no Angular Material.

Como configurar e utilizar o Angular Material
Como configurar e utilizar o Angular Mater

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
Navegador mostrando o conteúdo do endereço http://localhost:4200 - projeto Angular recém-criado
Projeto recém-criado

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/[email protected].
✔ Package information loaded.

The package @angular/[email protected] 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>
app.component.html

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 {}
app.module.ts

Executando o projeto novamente (npm start) podemos ver que é renderizado na tela um botão exatamente como na documentação do Angular Material:

Navegador mostrando o conteúdo do endereço http://localhost:4200 - um botão do Angular Material
Resultado

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:

Navegador mostrando o conteúdo do projeto deste artigo. Há diversos componentes do Angular Material e uma tabela mostrando a frequência de utilização do nome ao longo das décadas
Exemplo utilizando os componentes do Angular Material

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;
}
frequencia-nome.model.ts
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);
      }),
    );
  }
}
ibge.service.ts

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 {}
app.module.ts

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>
modal-frequencia-nome.component.html

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,
  };
}
modal-frequencia-nome.component.ts

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;
  }
}
app.component.ts

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>
app.component.html

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 {}
app.module.ts

Resultado até o momento:

Navegador exibindo o formulário e resultado da consulta do nome "marcelo"
Resultado

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;
  }
}
styles.scss

Resultado final:

Navegador mostrando o formulário e resultado da consulta em uma tabela.
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:

Link do projeto utilizado neste texto:

GitHub - marcelovismari/consolelog-configurar-utilizar-angular-material: Saiba como configurar e usar o Angular Material para aprimorar o design e a funcionalidade dos seus projetos Angular.
Saiba como configurar e usar o Angular Material para aprimorar o design e a funcionalidade dos seus projetos Angular. - GitHub - marcelovismari/consolelog-configurar-utilizar-angular-material: Saib…