DEV Community

loading...
Cover image for Creating a Search Filter in Angular

Creating a Search Filter in Angular

idrisrampurawala profile image Idris Rampurawala ・Updated on ・6 min read

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.

Angular filter demo

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 {
  /**
   * Transform
   *
   * @param {any[]} items
   * @param {string} searchText
   * @returns {any[]}
   */
  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 the search 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:
Angular Filter 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:

  1. Components β€” directives with a template.
  2. Structural directives β€” change the DOM layout by adding and removing DOM elements.
  3. 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 the searchText 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.

If you find this helpful or have any suggestions, feel free to comment. Also, do not forget to hit ❀️ or πŸ¦„ if you like my post.

See ya! until my next post πŸ˜‹

Discussion

pic
Editor guide
Collapse
othmangueddana profile image
Othman-Gueddana

thank you man <3

Collapse
inderoffcial profile image
inderoffcial

Hi can you explain how can we create Filter pipe for multiple columns and if some columns have null value.
TIA

Collapse
gusgonnet profile image
Gustavo

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

Collapse
idrisrampurawala profile image
Idris Rampurawala Author

Hey Gustavo,

Thanks for reading the article and for the compliments 😁

Collapse
guhuojin profile image
guhuojin

I tried but it complains appFilter cannot be found. What I am missing?

Collapse
idrisrampurawala profile image
Idris Rampurawala Author

Hi,

Thanks for following the post. To resolve your issue, could you please either paste the error screenshot here or better if you could create the issue over stackblitz.
This would help to quickly address and resolve your issues ☺️

Collapse
guhuojin profile image
guhuojin

I am using this FilterPipe in a sub-component. It turns out in my setting, this customized pipe needs to be referenced in the root module. Now it is working. This is very helpful, thank you!

Thread Thread
idrisrampurawala profile image
Idris Rampurawala Author

Glad that you were able to solve the issue πŸ˜‹

Also, please show some love to this post if you liked it by hitting ❀️ or πŸ¦„ if you haven't already done it 😁