How to filter results list in Angular with input text


Dev-Bookmarks Logo

(P) Bookmarks.dev - Open source Bookmarks and Code Snippets Manager for Developers & Co. See our How To guides to help you get started. Public Bookmarks Repo on Github - Star


I find myself lately looking for recent visited bookmarks via the Ctrl+h - History dialog on Bookmarks.dev. To make my life even easier, I added a filter box in the dialog. You can now add one or more keywords to filter the displayed results even further. One thing let to another and I have added the filter box to other bookmarks lists, like Pinned, ReadLater or My Dashboard:

show filter animation

In this blog post I will present the implementation in Angular required to achieve this new feature.

Octocat Source code for bookmarks.dev is available on Github - Star

The bookmarks filter pipe code

The easiest way to achieve the filtering functionality is to use an angular pipe1:

<mat-expansion-panel *ngFor="let bookmark of bookmarks | bookmarkFilter: filterText">

The complete implementation of the pipe is:

// bookmarks-filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { Bookmark } from '../core/model/bookmark';

@Pipe({name: 'bookmarkFilter'})
export class BookmarksFilterPipe implements PipeTransform {
  /**
   * Bookmarks in, bookmarks out that contain all the terms in the filterText
   *
   * @param {Bookmark[]} bookmarks
   * @param {string} filterText
   * @returns {Bookmark[]}
   */
  transform(bookmarks: Bookmark[], filterText: string): Bookmark[] {
    if (!bookmarks) {
      return [];
    }
    if (!filterText) {
      return bookmarks;
    }

    return bookmarks.filter(bookmark => {
      return this.bookmarkContainsFilterText(bookmark, filterText);
    });
  }

  private bookmarkContainsFilterText(bookmark: Bookmark, filterText): boolean {
    filterText = filterText.toLocaleLowerCase();
    const filterTerms = filterText.split(' ');
    for (const filterTerm of filterTerms) {
      const hasFilterTerm = this.bookmarkContainsFilterTerm(bookmark, filterTerm);
      if (hasFilterTerm === false) {
        return false;
      }
    }

    return true;
  }

  private tagsHaveFilterText(tags: string[], filterText: string): boolean {
    for (const tag of tags) {
      if (tag.includes(filterText)) {
        return true;
      }
    }

    return false;
  }

  private bookmarkContainsFilterTerm(bookmark: Bookmark, filterTerm: string) {
    return bookmark.name.toLocaleLowerCase().includes(filterTerm)
      || bookmark.location.toLocaleLowerCase().includes(filterTerm)
      || bookmark.description.toLocaleLowerCase().includes(filterTerm)
      || this.tagsHaveFilterText(bookmark.tags, filterTerm);
  }
}

It checks if the bookmarks contains all the filter terms provided in the filterText either in title, location, tags or description of the bookmark.

The filterText it’s split into terms by spaces and all terms must be present

Usage in the Angular History Dialog Component

Below is the complete usage of the bookmarkFilter in the history dialog html component:

<!--
 hot-keys-dialog.component.html
-->
<div class="dialog-title">
  <h2 mat-dialog-title [innerHTML]="title"></h2>
  <div class="form-group has-search">
    <span class="fas fa-filter form-control-feedback"></span>
    <input type="search" [(ngModel)]="filterText" class="form-control" placeholder="Filter...">
  </div>
</div>
<mat-dialog-content *ngIf="(bookmarks$ | async) as bookmarks" class="mt-2 pt-1 pb-1">
  <mat-accordion>
    <mat-expansion-panel *ngFor="let bookmark of bookmarks | bookmarkFilter: filterText">
      <mat-expansion-panel-header>
        <div class="p-3">
          <h5 class="card-title">
            <a href=""
               [innerHTML]="bookmark.name | slice:0:100 | highlightHtml: filterText"
               target="_blank"
               (click)="addToHistoryService.promoteInHistoryIfLoggedIn(true, bookmark)"
               (auxclick)="addToHistoryService.onMiddleClickInDescription(true, $event, bookmark)"
            >
              see innerhtml
            </a>
            <sup class="external-link-hint"><i class="fas fa-external-link-alt"></i></sup>
          </h5>
          <h6 class="card-subtitle mb-2 text-muted url-under-title"
              [innerHTML]="bookmark.location | slice:0:120 | highlightHtml: filterText"
          >
            see innerhtml
          </h6>
        </div>
      </mat-expansion-panel-header>

      <ng-template matExpansionPanelContent>
        <app-bookmark-text [bookmark]="bookmark"
                           (click)="addToHistoryService.onClickInDescription(true, $event, bookmark)"
                           (auxclick)="addToHistoryService.onMiddleClickInDescription(true, $event, bookmark)">
        </app-bookmark-text>
      </ng-template>
    </mat-expansion-panel>
  </mat-accordion>
</mat-dialog-content>

The filterText variable is a two-way bounded variable - <input type="search" [(ngModel)]="filterText" class="form-control" placeholder="Filter...">. The value of the html input is filter parameter filter of the bookmark filter pipeline as seen before - transform(bookmarks: Bookmark[], filterText: string): Bookmark[].

Note the highlight pipe chained to highlight the search terms: [innerHTML]="bookmark.name | slice:0:100 | highlightHtml: filterText"

In the component filterText is defined as a simple string variable:

export class HotKeysDialogComponent implements OnInit {

  bookmarks$: Observable<Bookmark[]>;
  title: string;
  filterText: '';

  constructor(
    private dialogRef: MatDialogRef<HotKeysDialogComponent>,
    public addToHistoryService: AddToHistoryService,
    @Inject(MAT_DIALOG_DATA) data
  ) {
    this.bookmarks$ = data.bookmarks$;
    this.title = data.title;
  }

  ngOnInit() {
  }
}

Bonus: the Highlight Pipe

You can find below the implementation of the Highlight pipe, which highlights the filter terms in the history dialog:

import {Pipe} from '@angular/core';
import {PipeTransform} from '@angular/core';

@Pipe({ name: 'highlightHtml' })
export class HighLightHtmlPipe implements PipeTransform {

  transform(text: string, search): string {
    if (!search || search === undefined) {
      return text;
    } else {
      let pattern = search.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
      pattern = pattern.split(' ').filter((t) => {
        return t.length > 0;
      }).join('|');
      pattern = '(' + pattern + ')' + '(?![^<]*>)';
      const regex = new RegExp(pattern, 'gi');

      return search ? text.replace(regex, (match) => `<span class="highlight">${match}</span>`) : text;
    }
  }

}

Conclusion

In this post you’ve seen a way to dynamically filter a list of elements in Angular with the helps of pipes.

If you liked this, please show some love and give us a star Star

References

Subscribe to our newsletter for more code resources and news

Adrian Matei

Adrian Matei
Life force expressing itself as a coding capable human being

Complete example of a CRUD API with Express GraphQL

CRUD API example implemented with GraphQL-js and expresss-graphql acting as integration point to the existing REST API supporting Bookmarks.dev Continue reading