Create Generic Angular Pipes | pure & impure

Simar Paul Singh
Simar's blog
Published in
4 min readAug 10, 2018

--

Fulfill your pipe dream in templates

  1. Keep one-off (non-reusable) transform impure functions in components, pass them as value-argument to be applied
  2. Define your re-usable pure functions separately, and import them in components as needed and pass them as value-argument to be applied

For source click on [GitHub] & To Try out click on [CodePen]

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

@Pipe({
name: 'applyPure',
pure: true // immutable (value) inputs & pure fn (function)
})
export class ApplyPurePipe implements PipeTransform {

transform(value: any, fn: Function): any {
return fn(value);
}
}

@Pipe({
name: 'apply',
pure: false // any (value) inputs & any fn (function)
})
export class ApplyPipe implements PipeTransform {

transform(value: any, fn: Function): any {
return fn(value);
}

}

Write your functions in components, and pass the function itself as pipe value-arg to be applied

@Component({
selector: 'my-app',
template: `<p>SUM of {{fib | json}} = {{fib | apply: sum}}</p>`
})
class AppComponent {
fib = [1, 2, 3, 5, 8];public sum(collection: [number]): number {
return collection.reduce((first, second) => first + second);
}

}

Why do we need two kinds (apply & applyPure)?

Pure pipes leverage many advantages which come with Pure functions

  1. First pure pipes evaluate only when input changes, second they cache the outputs for previously evaluated inputs, and can bind the result from cache without re-evaluating the pipe expression if the same input was previously evaluated.
  2. Single instance of a pure pipe is used for all bindings in the app, across components
  3. Just need to test transform function, known input to known output.

Impure pipes can’t leverage caching, instance re-use and simple tests

When should we declare a Pipe as {pure: false}?

  1. Either, the transform function they are evaluating isn’t pure.
  2. Or, there is no way to identify or differentiate between different inputs.

(1) is obvious but (2) is something easy to trip over. Let us see it with examples

Example (1): Consider an example, where apply metric to/from imperial unit conversion on mass (kg. to/from lbs)

For source of this example click on [GitHub] & To try out, click on [CodePen]

import { Component } from '@angular/core';

@Component({
selector: 'app-apply-pipe',
templateUrl: './apply-pipe.component.html',
styleUrls: ['./apply-pipe.component.css']
})
export class ApplyPipeComponent {

value = 0;
fromScale: 'kg' | 'lbs' = 'kg';
toScale: 'kg' | 'lbs' = 'lbs';
toggle = false;

constructor() {}
covertValue = (v) => {
return this.toScale === 'kg' ? v / 2.2 : v * 2.204;
}
toggleScale() {
this.toggle = !this.toggle;
this.fromScale = this.toggle ? 'lbs' : 'kg';
this.toScale = this.toggle ? 'kg' : 'lbs' ;
}

}

Try it out on clicking on clicking on [CodePen]

<label for="value">Input {{fromScale}}
<input id="value" [value]="value" type="number" (input)="value = $event.target.value" >
</label>

<button (click)="toggleScale()">Convert to {{toScale}}</button>
<div>
{{value}} {{fromScale}} = {{value | applyPure: covertValue}} {{toScale}}
</div>
<div>
{{value}} {{fromScale}} = {{value | apply: covertValue}} {{toScale}}
</div>

Note: component.convertValue(number) is not a pure function. It not only depends on the input number but also component.toScale which it closes over is defined outside of the function on the component.

With applyPure Pipe :(

Since applyPure pipe, the conversion pipe is only applied if input changes, not when the scale toggles

With apply Pipe :)

With apply pipe {pure: false}, the conversion pipe is applied with standard change detection, which detects changes to both, input and the scale

To try it out, click here

Example (2) : Consider a pipe that sums given array of Fibonacci sequence

Checkout source by clicking on [Github]

import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-apply-pure-pipe',
templateUrl: './apply-pure-pipe.component.html',
styles: []
})
export class ApplyPurePipeComponent {

constructor() { }

fib = [1, 2];
color: 'red' | 'green' = 'green';

pushNextNumber() {
const lastIndex = this.fib.length - 1 ;
this.fib.push(this.fib[lastIndex] + this.fib[lastIndex - 1]);
this.color = 'red'; // 'red' means fib array (modified)
}

recreateWithNextNumber() {
const lastIndex = this.fib.length - 1 ;
this.fib = [...this.fib, this.fib[lastIndex] + this.fib[lastIndex - 1] ];
this.color = 'green'; green means new fib array is (new)
}

public sum(collection: [number]): number {
return collection.reduce((first, second) => first + second);
}

}

Note: Even though sum() is a pure function, angular puts another restriction on pure pipes; the input must change by reference (new object) for pipes to reevaluate

<button (click)="recreateWithNextNumber()">Generate new array with Next Fibonacci Number</button><button (click)="pushNextNumber()">Push Next Fibonacci Number to Same Array</button><div>
SUM of {{fib | json}} = {{fib | applyPure: sum}}
</div>
<div>
SUM of {{fib | json}} = {{fib | apply: sum}}
</div>

Try it out on clicking on [Code Pen]

With applyPure Pipe :)

SUM of {{fib | json}} = {{fib | applyPure: sum}}

With applyPure pipe, the sum is applied only when the input array changes by reference (new Array), not when we mutate the existing array by pushing the next number

With apply Pipe :)

SUM of {{fib | json}} =  {{fib | apply: sum}}

With apply pipe {pure: false}, the sum(arr) is applied with standard change detection, which detects changes in both cases, when a new number is pushed to an existing array object and also when we get a new Array object

--

--