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 until either the time is passed in as the second value or 500 ms goes by 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 checke 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.