5 usage ideas for Angular pipes

Pipes are a useful feature in Angular. They are a simple way to transform values in an Angular template. There are some built in pipes, but you can also build your own pipes.

A pipe takes in a value or values and then returns a value. This is great for simple transformations on data but it can also be used in other unique ways. This post will highlight a few useful or unique use cases that we found for pipes.

The ratings on each of the points indicate how much I believe this particular use case abuses the pipe framework.

1. Return Default Values

The functionality behind a Default Pipe is pretty self explanatory—if a value is falsy, use a default value instead. The implementation is also very simple.

@Pipe({name: 'default', pure: true})
export class DefaultPipe {
   transform(value: any, defaultValue: any): any {
       return value || defaultValue;
   }
}

The default pipe uses Angular’s ability to pass multiple values into the pipe to get both the value and the default value.

An example of using the default pipe in Angular would be:

<lucid-icon [name]="folder.icon | default:'Folder'"></lucid-icon>

In the example above, either the folder has an icon that is used or the Folder icon is used as a default. You can play with the example code using the Default Pipe in the Plunker below.

Hackiness Rating Description
This use is along the lines of what pipes are intended to do.

2. Debounce Input

A Debounce Pipe is much more technically interesting than the Default Pipe. The basic premise of the idea is that the passed-in value can change frequently, but the actual value returned from the pipe will not change until the value has remained changed for a certain period of time. This can be very useful when listening to user input but not wanting to update the user interface until after the user has finished typing.

You can see the implementation below:

@Pipe({name: 'debounce', pure: false})
export class DebouncePipe {
   private currentValue: any = null;
   private transformValue: any = null;
   private timeoutHandle: number = -1;

   constructor(
       private changeDetector: ChangeDetectorRef,
       private zone: NgZone,
   ) {
   }

   transform(value: any, debounceTime?: number): any {
       if (this.currentValue == null) {
           this.currentValue = value;
           return value;
       }
       if (this.currentValue === value) {
           // there is no value that needs debouncing at this point
           clearTimeout(this.timeoutHandle);
           return value;
       }
       if (this.transformValue !== value) {
           // there is a new value that needs to be debounced
           this.transformValue = value;
           clearTimeout(this.timeoutHandle);
           this.timeoutHandle = setTimeout(() => {
               this.zone.run(() => {
                   this.currentValue = this.transformValue;
                   this.transformValue = null;
                   this.changeDetector.markForCheck();
               });
           }, typeof debounceTime == 'number' ? debounceTime : 500);
       }
       return this.currentValue;
   }
}

The Debounce Pipe takes in a value, and then if the value has changed since the last time the debounce completed (or it is null), it will wait either the time passed in as the second value or 500 ms if a time wasn’t provided, and then apply the new value.

An example of using it in Angular would be:

<div
    *ngIf="hasInputError(contentOption) | debounce"
    class="error-message"
>
    {{errorMessage(contentOption)}}
</div>

You can play with the example code using the Debounce Pipe in the Plunker below.

Hackiness Rating Description
This pipe doesn’t generally follow the idea of taking in a value and returning a result value. Instead, it delays returning in the latest passed-in value, which means it has to be an impure pipe because it returns different values for the same input depending on the time. However, generally it works well and is not too abusive of the framework.

3. Get the Position of an Element

Angular has a handy feature that lets you assign an element or component to a variable and then reference that variable within that template. The Element Position Pipe takes advantage of that feature and allows you to pass an element to the pipe and have it return the position. This can be a useful feature for deciding where to position a pop-up or some other element.

Here is the implementation:

@Pipe({name: 'elementPosition', pure: true})
export class ElementPosition {
   transform(value: HTMLElement, xLerp: number, yLerp: number): Point|null {
       if (value != null) {
           const boundingRect = value.getBoundingClientRect();
           return {
               x: boundingRect.left + xLerp * boundingRect.width,
               y: boundingRect.top + yLerp * boundingRect.height,
           };
       } else {
           return null;
       }
   }
}

The two numbers that are passed in with the element are used to decide where on the element the position should be. The first value is used in the x position and is multiplied by the width. So if you want to get the position of the right side of the element, you would pass in 1 for the first parameter, which would mean x + 1 * width. The second parameter is the same but for the y value and the height.

An example of using the Element Position Pipe in Angular would be:

<div
   #titleElement
   (click)="expanded = !expanded"
>
   {{title}}
</div>
<ng-container popup [visible]="expanded">
   <lucid-menu
       *popupContent
       [items]="menuOptions"
       [position]="titleElement | elementPosition:0:1"
   ></lucid-menu>
</ng-container>

 

In this simplified snippet, the menu is getting the bottom-left position of the `titleElement` passed into its `[position]` input.

Hackiness Rating Description
This use is fairly hacky because it doesn’t work in Web Worker or service-side environments. It also returns a position that is relative to the browser window and not the nearest relative or absolute positioned ancestor.

4. Feign Natural Typing

A flashy feature that I have seen throughout the web is animating text as if it was being typed by a user. An intuitive way to implement such a feature in Angular would be using pipes.

Below is a simple implementation of a Natural Typing Pipe:

@Pipe({name: 'naturalType', pure: false})
export class NaturalType {
   private typed: string = '';
   private target: string = '';
   private currentIndex: number = -1;
   private timeoutHandle: number = -1;

   constructor(
       private changeDetector: ChangeDetectorRef,
       private zone: NgZone,
   ) {
   }

   transform(value: string, mintypingSpeed: number = 30): any {
      if (this.target !== value) {
       clearTimeout(this.timeoutHandle);
       this.typed = '';
       this.currentIndex = -1;
       this.target = value;
       this.typeNextCharacter(mintypingSpeed);
      }
      return this.typed;
   }
   
   private typeNextCharacter(mintypingSpeed: number) {
    this.currentIndex++;
    this.typed = this.target.substr(0, this.currentIndex);
    this.changeDetector.markForCheck();
    if (this.typed !== this.target) {
      const time = Math.round(Math.random() * 70) + mintypingSpeed;
      this.timeoutHandle = setTimeout(()=> {
        this.zone.run(() => this.typeNextCharacter(mintypingSpeed));
      },time);
    }
   }
}

Usage of the pipe is pleasantly simple—simply pass a string through the pipe.

{{value | naturalType}}

You can play with the example code using the Natural Typing Pipe in the Plunker below.

Hackiness Rating Description
In my opinion, this is actually a fairly elegant way to implement this feature. However, it does have to be an impure pipe because the value that it returns is based on the input and time, not just the input value.

5. Track User Input

Analytics and recently used lists are two features that come to mind when I think of tracking user input. It seems possible to do that with a pipe. So why not?

First, we will need a tracking service.

@Injectable()
export class TrackingService {
  private wordsUsed: Set = new Set();
  
  public addWordUsed(word: string) {
    this.wordsUsed.add(word);
  }
  
  public getWords(): string[] {
    return Array.from(this.wordsUsed);
  }
 
}

Then we will need a pipe to feed the service.

@Pipe({name: 'track', pure: true})
export class TrackingPipe {
   constructor(
       private trackingService: TrackingService,
   ) {
   }

   transform(value: string): string {
       this.trackingService.addWordUsed(value);
       return value;
   }
}

Consuming the feed is then simple with Angular.

@Component({
  ...
  template: `
    ... 
    <div *ngFor="let word of getWords()">{{word}}</div>
  `,
})
export class AppComponent {
  ...
  constructor(private trackingService: TrackingService) {
    
  }
  
  public getWords(): string[] {
    return this.trackingService.getWords();
  }
}

After running, it doesn’t render the list immediately. Checking the console we have this error: ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. This error makes sense. When the change detector runs, it transforms the value using the tracking pipe which modifies the list. Since the value changed during change detection, the change detection is then out of date and has to run again. If it changes during change detection … ∞. Since we were smart ?, we put the debounce pipe that we built earlier in front of the tracker pipe ({{inputValue | debounce:200 | track}}), so we know that the value isn’t going to actually change every change detection and cause an infinite loop. So we can just make our logging run after an async callback, and then everything should work.

@Pipe({name: 'track', pure: true})
export class TrackingPipe {
   constructor(
       private trackingService: TrackingService,
       private changeDetector: ChangeDetectorRef,
       private zone: NgZone,
   ) {
   }

   transform(value: string): string {
       Promise.resolve().then(() => {
        this.trackingService.addWordUsed(value);
        this.zone.run(() => this.changeDetector.markForCheck()); 
       });
       return value;
   }
}

If we wanted to, we could even log the values over the network and then we wouldn’t have to fake the async! ?

You can play with the example code using the Tracking Pipe in the Plunker below.

Hackiness Rating Description
This pipe doesn’t actually do any transformations on the value, which breaks the whole paradigm of a pipe. It has to pretend to log values in an asynchronous way in order to not modify component state during change detection. It is stateful. There is no benefit over just injecting the service into the component and logging the values there. But … it was possible and we did it ?.

5 Comments

  1. I remember in the good ol’ Angular 1 days pipes I abused pipes way worse than this haha. I think every did, and it was part of the reason Angular 2 downplayed the importance of pipes and included less default pipes (like the the sorting and filter pipes).

  2. […] a recent post, we showed off some examples of Angular pipes we use in our code base. Now, we’d like to talk about directives. Angular directives allow you to […]

  3. how can this debounce pipe be modified to update the ngModel property after a specific interval instead of using it to control displaying the interpolated property.

    Also It could be pretty sweet if this can debounce ngModelChange event but I doubt that’s possible with pipes.

  4. The second example I have reworked.

    Case boolDebouce:
    I have animation, and, for example, when something is hidden and appears using animation, why keep it in the DOM when it is hidden. So if you can enable the element immediately before it appears, then when opening the element, you need to disable it after the animation is complete.

    @Pipe({name: ‘debounce’, pure: false})
    export class DebouncePipe {
    private currentValue: any = null;
    private transformValue: any = null;
    private timeoutHandle = -1;

    constructor(
    private changeDetector: ChangeDetectorRef,
    private zone: NgZone,
    ) {
    }

    transform(value: any, debounceTime?: number): any {
    if (this.currentValue == null) {
    this.currentValue = value;
    return value;
    }
    if (this.currentValue === value) {
    // there is no value that needs debouncing at this point
    this.transformValue = null;
    clearTimeout(this.timeoutHandle);
    return value;
    }
    if (this.transformValue !== value) {
    // there is a new value that needs to be debounced
    this.transformValue = value;
    clearTimeout(this.timeoutHandle);
    // @ts-ignore
    const timeout = typeof debounceTime === ‘number’ ? debounceTime : 500;
    if (timeout == 0) {
    this.currentValue = this.transformValue;
    this.transformValue = null;
    this.changeDetector.markForCheck();
    return this.currentValue;
    } else {
    this.timeoutHandle = setTimeout(() => {
    this.zone.run(() => {
    this.currentValue = this.transformValue;
    this.transformValue = null;
    this.changeDetector.markForCheck();
    });
    }, timeout);
    }
    }
    return this.currentValue;
    }
    }

    @Pipe({name: ‘boolDebounce’, pure: false})
    export class BoolDebouncePipe {
    private currentValue: boolean = null;
    private transformValue: boolean = null;
    private timeoutHandle = -1;

    constructor(
    private changeDetector: ChangeDetectorRef,
    private zone: NgZone,
    ) {
    }

    transform(value: boolean, debounceTimeIfTrue?: number, debounceTimeIfFalse?: number): any {

    if (this.currentValue === null) {
    this.currentValue = value;
    return value;
    }

    if (this.currentValue === value) {
    // there is no value that needs debouncing at this point
    this.transformValue = null;
    clearTimeout(this.timeoutHandle);
    return value;
    }

    if (this.transformValue !== value) {
    // there is a new value that needs to be debounced
    this.transformValue = value;
    clearTimeout(this.timeoutHandle);
    let timeout = 500;
    if (value) {
    timeout = typeof debounceTimeIfTrue === ‘number’ ? debounceTimeIfTrue : 500;
    } else {
    timeout = typeof debounceTimeIfFalse === ‘number’ ? debounceTimeIfFalse : 500;
    }
    if (timeout == 0) {
    this.currentValue = this.transformValue;
    this.transformValue = null;
    this.changeDetector.markForCheck();
    return this.currentValue;
    } else {
    // @ts-ignore
    this.timeoutHandle = setTimeout(() => {
    this.zone.run(() => {
    this.currentValue = this.transformValue;
    this.transformValue = null;
    this.changeDetector.markForCheck();
    });
    }, timeout);
    }
    }
    return this.currentValue;
    }
    }

  5. You had a little mistake in the code of Idea number 4,
    In the beginning you wrote:
    private timeoutHandle: number = -1;
    But afterwards you used it (it was defined as a number but you used it like a function):
    this.timeoutHandle = setTimeout(()=> {
    this.zone.run(() => this.typeNextCharacter(mintypingSpeed));
    },time);

    we fixed it by changing the type to “any”
    Can you help us change it to do this cool thing infinitely

Your email address will not be published.