Filtro de tabela utilizando PipeTransform e DebounceTime - como filtrar os dados em tempo real - Angular 2+
Veja como filtrar os dados de uma tabela de forma dinâmica através da utilização de um PipeTransform e o operador debounceTime do RxJS.
Construção da service para consultar os dados
Para construir a base de estudo, vamos utilizar uma lista de distritos do estado de São Paulo disponibilizadas no seguinte link pelo IBGE:
https://servicodados.ibge.gov.br/api/v1/localidades/estados/35/distritos.
Este link exibe diversas informações, mas vamos focar apenas na utilização das informações id
e o nome
do distrito. Deste modo nossa service ficará da seguinte forma:
// distritos.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
const url =
'https://servicodados.ibge.gov.br/api/v1/localidades/estados/35/distritos';
@Injectable({providedIn: 'root'})
export class DistritosService {
constructor(private httpClient: HttpClient) { }
listar(): Observable<{ id: number, nome: string }[]> {
return this.httpClient
.get<any[]>(url)
.pipe(
map(dadosDaApi => {
return dadosDaApi.map(a => {
return {
id: a.id,
nome: a.nome
};
});
})
);
}
}
Apesar de não ser o foco do artigo, vale comentar sobre o map
. Sua utilização tem o objetivo de receber os dados brutos da API mas retornar apenas um novo objeto do tipo Array<{ id: number, nome: string }>
. A razão desta lógica é que vamos utilizar apenas estas duas informações por registro, id
e nome
.
Exibindo os dados na tabela
Com a service construída vamos focar na exibição dos dados na tela. Como vamos exibir os dados no AppComponent
, certifique-se de importar o módulo HttpClientModule
em seu AppModule
conforme abaixo:
// app.module.ts
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
],
imports: [
AppRoutingModule,
BrowserModule,
CommonModule,
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
// app.component.ts
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { DistritosService } from './services/distritos.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
distritos: Array<{ id: number, nome: string }> = [];
constructor(private distritosService: DistritosService) { }
ngOnInit() {
this.distritosService.listar().subscribe(
retornoApi => this.distritos = retornoApi
)
}
}
<!-- app.component.html -->
<html>
<head>
<title>Testes</title>
</head>
<body>
<table>
<tr *ngFor="let distrito of distritos">
<td>{{distrito.id}}</td>
<td>{{distrito.nome}}</td>
</tr>
</table>
<router-outlet></router-outlet>
</body>
</html>
Após subir a aplicação, ng serve
, podemos visualizar o resultado na tela do navegador:
Filtrando os dados
Agora que os dados estão renderizados e a lista de distritos já está armazenada no AppComponent
na variável distritos
, vamos criar um campo de texto para que o usuário possa filtrar os dados na tela.
Primeiro vou mostrar a forma mais comum de se encontrar essa implementação, pelo menos no meu cotidiano de desenvolvedor, depois a forma correta de executar esta implementação.
Vamos adicionar um campo de texto para o usuário poder digitar o que ele está buscando. Vamos também adicionar um evento para poder disparar o mecanismo de filtro:
<!-- app.component.html -->
<!-- ... (código ocultado) ... -->
<input
type="search"
placeholder="busca..."
(change)="filtrar($event.target.value)">
<table>
<tr *ngFor="let distrito of distritos">
<td>{{distrito.id}}</td>
<td>{{distrito.nome}}</td>
</tr>
</table>
<!-- ... (código ocultado) ... -->
Agora vamos criar o método filtrar
para de fato filtrar os dados se baseando no que o usuário digitou no campo de busca:
// app.component.ts
// ... (código ocultado) ...
filtrar(palavraChave: string) {
if (palavraChave) {
palavraChave = palavraChave.toUpperCase();
this.distritos = this.distritos.filter(a =>
a.nome.toUpperCase().indexOf(palavraChave) >= 0
);
}
}
// ... (código ocultado) ...
Se você analisou o código acima e entendeu a lógica, provavelmente já percebeu que somente a primeira busca irá funcionar. Veja que após a primeira busca a variável distritos
recebe o resultado filtrado e com isto perdemos a lista completa.
Veja a animação na imagem abaixo deste comportamento:
Para corrigir este comportamento teríamos que criar uma outra variável para guardar a lista completa de distritos e aumentar algumas linhas de código para controlar as duas listas (uma lista completa e outra filtrada). Vamos economizar linhas de código e utilizar um pipe para simplificar esta tarefa.
Se você não está acostumado com a criação de pipes, sugiro dar uma lida neste artigo: https://consolelog.com.br/como-construir-um-pipe-para-formatar-cpf-em-um-projeto-angular2/
Utilizando um pipe para filtrar os dados
O objetivo do nosso pipe é receber dois argumentos:
- lista completa de distritos
- palavra-chave que será utilizada para filtrar os dados - será o que o usuário digitar
O retorno será a lista de distritos filtrada. Nosso PipeTransform ficará no seguinte formato:
// array-filtro.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'arrayFiltro'
})
export class ArrayFiltroPipe implements PipeTransform {
transform(value: Array<any>, filtro: string): any {
if (filtro) {
filtro = filtro.toUpperCase();
return value.filter(a =>
a.nome.toUpperCase().indexOf(filtro) >= 0
);
} else {
// Quando filtro for vazio ou nulo,
// retornamos o próprio array
return value;
}
}
}
Para não nos esquecermos, vamos registrar este pipe no AppModule
:
// app.module.ts
// ... (código ocultado) ...
import { ArrayFiltroPipe } from './array-filtro.pipe';
@NgModule({
declarations: [
AppComponent,
ArrayFiltroPipe, // <<< registro do ArrayFiltroPipe
],
// ... (código ocultado) ...
E por fim as alterações no AppComponent
:
// app.component.ts
// ... (código ocultado) ...
import { Component, OnInit } from '@angular/core';
import { DistritosService } from './services/distritos.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
distritos: Array<{ id: number, nome: string }> = [];
filtro: string = ''; // << Variável que irá guardar
// o que o usuário digitou
constructor(private distritosService: DistritosService) { }
ngOnInit() {
this.distritosService.listar().subscribe(
retornoApi => this.distritos = retornoApi
)
}
}
// ... (código ocultado) ...
<!-- app.component.html -->
<!-- ... (código ocultado) ... -->
<input
type="search"
placeholder="busca..."
(keyup)="filtro = $event.target.value">
<table>
<tr *ngFor="let distrito of (distritos | arrayFiltro : filtro)">
<td>{{distrito.id}}</td>
<td>{{distrito.nome}}</td>
</tr>
</table>
<!-- ... (código ocultado) ... -->
Veja no template acima que a cada evento keyup modificamos o valor da variável filtro
com o valor do <input>
. Como a variável filtro
é utilizada em nosso arrayFiltro
(pipe), toda vez que ela (variável) tem seu valor alterado nosso pipe entra em ação para filtrar os dados:
Utilizando o debounceTime para melhorar a experiência
Veja que na animação da imagem acima, assim que o usuário digita algum caracter o mecanismo de filtro é ativo. No nosso exemplo isto não é um problema porque a lista de dados já está em memória, porém, isto pode ser um grande problema se a cada ciclo de filtragem for necessário efetuar uma requisição para obter nossos dados ou mesmo se a lista carregada em memória for muito grande. Já imaginou se a cada tecla pressionada houver uma requisição a uma API?
Para melhorarmos este mecanismo, vamos ativar a lógica de filtragem apenas quando houver um intervalo maior que x
milisegundos entre cada keyup
. Normalmente este intervalo indica que o usuário digitou parcialmente ou completamente o que está buscando e aguarda uma atualização na tela.
Esta lógica pode ser um pouco complexa para ser implementada, mas graças ao operador debounceTime
do RxJS
não precisamos nos preocupar com esta implementação. Veja abaixo as alterações:
No template removemos o (keyup)
e adicionamos o #campoBusca
. Este #campoBusca
serve para pegarmos a referência do <input>
na classe através do @ViewChild
:
<!-- app.component.html -->
<!-- ... (código ocultado) ... -->
<input
#campoBusca
type="search"
placeholder="busca...">
<table>
<tr *ngFor="let distrito of (distritos | arrayFiltro : filtro)">
<td>{{distrito.id}}</td>
<td>{{distrito.nome}}</td>
</tr>
</table>
<!-- ... (código ocultado) ... -->
Na classe AppComponent
adicionamos o evento (keyup
) ao <input>
utilizando o fromEvent
do RxJS:
// app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit, AfterViewInit {
distritos: Array<{ id: number, nome: string }> = [];
filtro: string = '';
@ViewChild('campoBusca') campoBusca: ElementRef<HTMLInputElement>;
constructor(private distritosService: DistritosService) { }
ngOnInit() {
this.distritosService.listar().subscribe(
retornoApi => this.distritos = retornoApi
)
}
ngAfterViewInit() {
fromEvent(this.campoBusca.nativeElement, 'keyup')
.pipe(
// Deixei um intervalo bem alto (2s)
// para ficar bem claro na animação.
debounceTime(2000)
)
.subscribe((e: Event) => {
const target = e.target as HTMLInputElement;
this.filtro = target.value;
});
}
}
Conforme comentado no código, deixamos o debounceTime
com um valor bem alto para ressaltar seu efeito na tela, veja no gif abaixo:
Considerações
Veja que a utilização do PipeTransform
tornou a implementação muito mais simples na filtragem dos dados. Um outro ponto importante foi a utilização do debounceTime
para evitar processamento desnecessário, ou seja, esperamos o usuário digitar algo mais concreto para então ativarmos nosso fluxo de filtragem.
O conteúdo deste post pode e será muito útil para você que é desenvolvedor e usa o Angular 2+. Não deixe de se aprofundar no assunto e expandir seu conhecimento para outros operadores do RxJS.
Links interessantes: