DEV Community

loading...
Cover image for How to easily filter bookmarks list with Angular pipes
Bookmarks.dev

How to easily filter bookmarks list with Angular pipes

ama profile image Adrian Matei Originally published at codepedia.org Updated on ・4 min read

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 functionality

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

The source code for Bookmarks.dev is available on Github

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">
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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="{{bookmark.location}}"
               [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>
Enter fullscreen mode Exit fullscreen mode

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() {
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

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 have found this useful, please show some love and give us a star on Github

References


  1. https://angular.io/guide/pipes 

Discussion

pic
Editor guide