Creating A Pipe That Can Consume Component Methods In Angular 4.4.0-RC.0
One of the nice things about "pure" Pipes in Angular 4 is that they are only called when their inputs change. In constrast, a component method, when invoked within a component template, is run at least once in every change detection cycle (for the given component). As such, I thought it would be interesting to try and combine the two concepts: what if we could create an Angular Pipe that invoked a component method; but, only when the inputs to that method changed. This would allow us to, essentially, create on-demand Pipe functionality using our component methods.
Run this demo in my JavaScript Demos project on GitHub.
Essentially, what I want to do is create a Pipe that turns around and invokes a public method on the current component. Something like:
{{ value | fn:formatValue }}
... where "value" is the input and "formatValue" is the public method on the current component. The "fn" pipe, being pure, would only pass "value" to the "formatValue" method when either the value or the formatValue() references change. And, since the formatValue() reference will never change, it means the "fn" pipe will only be invoked when the "value" changes.
Creating this "fn" Pipe is rather simple:
// Import the core angular services.
import { Pipe } from "@angular/core";
import { PipeTransform } from "@angular/core";
@Pipe({
name: "fn",
pure: true
})
export class FnPipe implements PipeTransform {
// I pass the first and rest arguments to the given function reference. This pipe
// is designed to be used in a template to access a component method:
// --
// In a template: {{ valueA | fn : componentMethodRef : valueB }}
// --
// ... becomes the invocation: null.componentMethodRef( valueA, valueB ).
public transform(
templateValue: any,
fnReference: Function,
...fnArguments: any[]
) : any {
// Due to the way pipes receive arguments, we may have inputs on both sides of
// the function reference. As such, let's join the two input sets.
fnArguments.unshift( templateValue );
// CAUTION: The function reference will NOT BE INVOKED IN THE COMPONENT CONTEXT.
// As such, a component must bind the reference if it needs to use the "this"
// scope within the function body.
return( fnReference.apply( null, fnArguments ) );
}
}
When a Pipe is invoked in Angular, it receives the input as its first argument; then, subsequent values as its rest arguments. This means that our function reference - the first "additional" argument - may be sandwiched by values intended to be arguments to the passed-in function reference. As such, we can create the complete function reference arguments by merging the first input into the rest inputs, less the function reference.
Notice, that I am invoking the function reference using the .apply() method. Since the fn Pipe doesn't have a reference to the contextual component instance, I'm executing the function reference in a null context. This means that the function reference won't have a "this" context during execution, unless the function is pre-bound to the component (which I am doing in the following code).
Now that we have our fn Pipe, let's use it in an Angular component. In the following App component, I'm going to be formatting a value in two ways: using our fn Pipe and using a vanilla method reference. I'm using the two approaches so we can see that the Pipe greatly reduces the number of times the formatting is applied:
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<p>
<strong>ThingOne</strong>: {{ message | fn:thingOne:"Sweeet!" }}
</p>
<p>
<strong>ThingTwo</strong>: {{ thingTwo( message, "Sweeet!" ) }}
</p>
<p>
<a (click)="setMessage( 'This is message One.' )">Use message one</a>
|
<a (click)="setMessage( 'This is message Two.' )">Use message Two</a>
</p>
`
})
export class AppComponent {
public message: string;
// I initialize the app component.
constructor() {
this.message = "Please select a message.";
}
// ---
// PUBLIC METHODS.
// ---
// I set the message for rendering.
public setMessage( newMessage: string ) : void {
this.message = newMessage;
}
// I transform the message using the FN PIPE.
// --
// CAUTION: Notice that this method is being defined using an ARROW FUNCTION. This
// is because the FN PIPE cannot apply the proper context (uses null) when invoking
// the function. As such, we need to pre-bind it to the component so that we can use
// the proper "this" reference when invoked via the FN PIPE.
public thingOne = ( value: string, suffix: string ) : string => {
console.info( "Calling thingOne()." );
// NOTE: Using this.join() just to demonstrate that the "this" reference works.
return( this.join( value.toUpperCase(), suffix ) );
}
// I transform the message using a standard method invocation.
public thingTwo( value: string, suffix: string ) : string {
console.warn( "Calling thingTwo()." );
// NOTE: Using this.join() just to demonstrate that the "this" reference works.
return( this.join( value.toUpperCase(), suffix ) );
}
// ---
// PRIVATE METHODS.
// ---
// I join the list of string values using a space.
private join( ...values: string[] ) : string {
return( values.join( " " ) );
}
}
As you can see, we are using two formatting approaches. One that uses the fn pipe:
{{ message | fn:thingOne:"Sweeet!" }}
... and one that uses a method invocation:
{{ thingTwo( message, "Sweeet!" ) }}
And, when we run this application and toggle the message back and forth, we can see how often these two methods are invokes:
It's a little hard to see what's going on here (the video makes it clear); but, I clicked on the same setMessage() call several times in a row. And, what we can see is that the "fn:thingOne" approach only invokes the thingOne() method when the message (input) changes. The thingTwo() approach, on the other hand, is invoked twice whenever I click the template link, even if the message hasn't been changed.
By using a pure Pipe to invoke a component method, we can leverage the memoized functionality of the pipe in order to reduce the number of times that a component method is called. While this may not be broadly applicable, I think this might be a very nice approach when you have complicated formatting that only ever applies to a single component. This would allow such specialization without having to create brand new pipes in Angular.
Want to use code from this post? Check out the license.
Reader Comments
Very nice trick. Its worth its own NPM package ... don't you think ?
@Ori,
Glad you like. That's an interesting idea to package it up. I'll look into that; I'm not exactly sure what the right-way to package Angular modules is. I'm sure there is a guide on it somewhere :)
We can to that together. Pick a sexier name for the pipe and I will create a github repo for the implementation
Great workaround. Definitely saving this.
@Ori,
Awesome - I'll circle back over the weekend!
@Dina,
Glad you like it! It seems like something that could be great for more involved formatted that only applies to a single component.
@All,
This implementation never quite sat right with me -- the fact that the method was being passed-around as a naked Function. I wanted to see if I could find a way to inject the Component reference into the Pipe for the .apply() call. I came up with "something" ... but, meh.
www.bennadel.com/blog/3443-experiment-injecting-a-component-reference-into-a-pipe-instance-in-angular-6-0-0.htm
I'd probably just use the Fat Arrow method binding.