Consider a scenario where we have a long list of data being displayed to the user on UI. It would be cumbersome for the user to search for any particular keyword in this long list without any search functionality provided. Hence, to make our users' life easy, we would usually implement search filters on our UI.
So now the question is, how to implement it? It's quite easy though ๐ All we want is a filter that takes an array as input and returns a subset of that array based on the term we supply. In Angular, this way of transforming data to some other form is implemented with Pipes
. Let's first understand a bit more about pipes
before we start the implementation.
Pipes in Angular
A pipe takes in data as input and transforms it into the desired output. A pipe can be used in both the HTML template expression and in a component. Angular does provide us with some built-in pipes such as CurrencyPipe, DatePipe, DecimalPipe, etc. Check this code snippet below to see it in action.
dateObj = Date.now();
// HTML template expression syntax using pipe operator (|)
{{ dateObj | date }} // output is 'Jun 15, 2015'
{{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
{{ dateObj | date:'shortTime' }} // output is '9:43 PM'
{{ dateObj | date:'mm:ss' }} // output is '43:11'
// Using in component
constructor(private datePipe: DatePipe) {
console.log(datePipe.transform(Date.now(),'yyyy-MM-dd'));
//2019-07-22
}
Pipes are of 2 types - Pure and Impure. For more information on Angular pipes, visit this link.
Implementing Search Filter
1. Create the Filter Pipe
Let's populate the pipe with code for the filter. Copy and paste this code into filter.pipe.ts
:
// filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'appFilter' })
export class FilterPipe implements PipeTransform {
/**
* Pipe filters the list of elements based on the search text provided
*
* @param items list of elements to search in
* @param searchText search string
* @returns list of elements filtered by search text or []
*/
transform(items: any[], searchText: string): any[] {
if (!items) {
return [];
}
if (!searchText) {
return items;
}
searchText = searchText.toLocaleLowerCase();
return items.filter(it => {
return it.toLocaleLowerCase().includes(searchText);
});
}
}
This pipe definition reveals the following key points:
- A pipe is a class decorated with pipe metadata.
- The pipe class implements the PipeTransform interface's transform method that accepts an input value followed by optional parameters and returns the -transformed value. In our filter pipe, it takes 2 inputs - an
array
and thesearch text
to filter the array with. - To tell Angular that this is a pipe, we apply the
@Pipe decorator
, which we import from the core Angular library. - The @Pipe decorator allows us to define the pipe name that we'll use within template expressions. It must be a valid JavaScript identifier. Our pipe's name is
appFilter
.
2. Using Pipe
To use the pipe, first, we need to import it into the app module. Our app.module.ts
file would now look like this:
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { FilterPipe } from './pipes/filter.pipe'; // -> imported filter pipe
@NgModule({
declarations: [
AppComponent,
FilterPipe // -> added filter pipe to use it inside the component
],
imports: [
BrowserModule,
FormsModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now we can use the filter pipe in our App Component
. Let's assume that in our app.component.html
we have an input box where we can enter our searchText
and a list that makes use of this pipe
to filter the results.
<!-- app.component.html -->
<div class="content" role="main">
<div class="card">
<div class="form-group">
<label for="search-text">Search Text</label>
<input type="email" class="form-control" id="search-text" aria-describedby="search-text"
[(ngModel)]="searchText" placeholder="Enter text to search"
autofocus>
</div>
<ul class="list-group list-group-flush">
<!-- results of ngFor is passed to appFilter with argument searchText -->
<li class="list-group-item" *ngFor="let c of characters | appFilter: searchText">
{{c}}
</li>
</ul>
</div>
</div>
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'angular-text-search-highlight';
searchText = '';
characters = [
'Ant-Man',
'Aquaman',
'Asterix',
'The Atom',
'The Avengers',
'Batgirl',
'Batman',
'Batwoman',
...
]
}
That's it! Now when we run our app, we will see the following output:
But hey! our search results are not being highlighted as it was shown at the beginning ๐
The reason is that Pipes
in angular only transforms the data passed to it into the desired output. It won't manipulate the HTML associated with it. To highlight the search results, we would be required to manipulate the HTML to highlight the searchText
part of it. This can be achieved using Directives
.
Directives in Angular
Angular directives are used to extend the power of the HTML by giving it new syntax. There are 3 types of directives:
- Components โ directives with a template.
- Structural directives โ change the DOM layout by adding and removing DOM elements.
- Attribute directives โ change the appearance or behavior of an element, component, or another directive.
Covering directives is outside the scope of this post. If you want to learn more about angular directives, visit this link.
Implementing Directive in our Application
In our case, we will be using the attribute directive
to highlight the searchText
in the result list.
1. Creating highlight directive
An attribute directive minimally requires building a controller class annotated with @Directive, which specifies the selector that identifies the attribute. The controller class implements the desired directive behavior.
Let's populate the directive with code for the highlighting. Copy and paste this code into highlight.pipe.ts
:
// highlight.directive.ts
import { Directive, Input, SimpleChanges, Renderer2, ElementRef, OnChanges } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective implements OnChanges {
@Input() searchedWord: string; // searchText
@Input() content: string; // HTML content
@Input() classToApply: string; //class to apply for highlighting
@Input() setTitle = false; //sets title attribute of HTML
constructor(private el: ElementRef, private renderer: Renderer2) { }
ngOnChanges(changes: SimpleChanges): void {
if (!this.content) {
return;
}
if (this.setTitle) {
this.renderer.setProperty(
this.el.nativeElement,
'title',
this.content
);
}
if (!this.searchedWord || !this.searchedWord.length || !this.classToApply) {
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', this.content);
return;
}
this.renderer.setProperty(
this.el.nativeElement,
'innerHTML',
this.getFormattedText()
);
}
getFormattedText() {
const re = new RegExp(`(${this.searchedWord})`, 'gi');
return this.content.replace(re, `<span class="${this.classToApply}">$1</span>`);
}
}
The logic is to manipulate the current HTML element by adding
<span>
tag in between thesearchText
and applying the highlighting class to it.
2. Using directive
To use the pipe, first, we need to import it into the app module. Our app.module.ts
file would now look like this:
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HighlightDirective } from './directives/highlight.directive'; // -> imported directive
import { FilterPipe } from './pipes/filter.pipe';
@NgModule({
declarations: [
AppComponent,
HighlightDirective, // -> added directive
FilterPipe
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
To use this directive in our HTML file, we will add it as a normal HTML attribute with all its parameters. It would look like this:
<!-- app.component.html -->
<div class="content" role="main">
<div class="card">
<div class="form-group">
<label for="search-text">Search Text</label>
<input type="email" class="form-control" id="search-text" aria-describedby="search-text"
[(ngModel)]="searchText" placeholder="Enter text to search"
autofocus>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item" *ngFor="let c of characters | appFilter: searchText"
appHighlight [searchedWord]="searchText" [content]="c"
[classToApply]="'font-weight-bold'" [setTitle]="'true'">
{{c}}
</li>
</ul>
</div>
</div>
Now, we would be able to see the desired output! ๐
You check out my GitHub repo for a complete implementation of this post.
See ya! until my next post ๐
Top comments (20)
thank you man <3
Hey! Nice tutorial mate, learned a lot about it. The highlight feature does not work for angular 12(no idea why). Cloned the project and tested it in Angular 11 and 12 and it only works in <12. Is there anything I can do for it to work in Angular 12?
Thanks for reading it. Glad you liked it. I'll try to check this issue. You can log it as an issue in GitHub.
I'm getting an error:
NG9: Property 'c' does not exist on type 'AppComponent'.
Since thec
is not in the AppComponent.Sorry, my bad :) I missed some property mapping in my example :)
Tenkyu
if there are no results found how to show it.
Thank you!
after text filter it need to be selected in input box while selecting search result, how we achieve in this?
Hi can you explain how can we create Filter pipe for multiple columns and if some columns have null value.
TIA
wow, Idris, you explained it so well and I just implemented this on my app in minutes thanks to your post - THANK YOU! you rock
Hey Gustavo,
Thanks for reading the article and for the compliments ๐
BTW, just an idea, but it would be nice to have a counter, counting the words matched :)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.