Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Justin Hinden
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Justin Hinden@JustinHinden )

Experiment: Injecting A Component Reference Into A Pipe Instance In Angular 6.0.0

By Ben Nadel on

A few months ago, I created a Pipe for Angular 4.4.0 that would pipe a value through a method of the current component. Only, it wasn't really a "method" in the bound sense - it was a free-floating, naked Function reference. This Function reference could be implicitly bound through the use of a Fat-Arrow function. But, even so, this never quite sat right with me. I wanted to see if I could find a way to inject the current Component reference into the Pipe instance such that I could use the component as the invocation context when calling .apply(). Ultimately, what I came up with wasn't really a satisfactory solution. But, for the purposes of exploration, I'm sharing it here.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

As a refresher on the concept, I wanted to find a way to leverage the caching of Pure Pipes such that I could run a Component template value through a Component method without having that method get invoked on every digest cycle:

{{ someViewModelValue | fn:someComponentMethod }}

Ideally, it would be great if unnecessary digest cycles were controlled with a Component's OnPush ChangeDetectionStrategy. But, that's not always possible in the current context. As such, using a Pure Pipe's caching feature could provide some performance benefit in a context where change-detection is running more frequently.

After a few hours of poking around, I couldn't find any way for the FnPipe to ask for the contextual Component in a generic way. Instead, I had to create a Dependency-Injection token that would explicitly provide the Component to the FnPipe:

  • // Import the core angular services.
  • import { Optional } from "@angular/core";
  • import { Pipe } from "@angular/core";
  • import { PipeTransform } from "@angular/core";
  • import { Self } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // I provide a dependency-injection token for the Fn pipe execution context.
  • export class FnPipeContext {
  • // ...
  • }
  •  
  • @Pipe({
  • name: "fn",
  • pure: true
  • })
  • export class FnPipe implements PipeTransform {
  •  
  • private context: any;
  •  
  • // I initialize the fn-pipe.
  • // --
  • // NOTE: We are injecting an OPTIONAL context for function execution.
  • constructor( @Optional() @Self() context: FnPipeContext ) {
  •  
  • this.context = context || null;
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // 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: context.componentMethodRef( valueA, valueB ).
  • public transform(
  • headArgument: any,
  • fnReference: Function,
  • ...tailArguments: any[]
  • ) : any {
  •  
  • // Due to the way pipes receive arguments, we can have inputs on both sides of
  • // the function reference. As such, let's join the two input sets when invoking
  • // the given Function reference.
  • return( fnReference.apply( this.context, [ headArgument, ...tailArguments ] ) );
  •  
  • }
  •  
  • }

Here, you can see that the Constructor for the FnPipe class is requesting a value of type "FnPipeContext". This injected value, if it exists, is then used when calling .apply() on the fnReference transform() Function parameter.

By default, there won't be any Provider for the FnPipeContext token. As such, the contextual Component has to assign itself as the FnPipeContext class in the local injector:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { FnPipeContext } from "./fn.pipe";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • // Here, we can provide special services that are available in the component
  • // injector. In this case, we're telling the FnPipe to use the AppComponent instance
  • // as the context when executing the "fn" function reference.
  • viewProviders: [
  • {
  • provide: FnPipeContext,
  • useClass: AppComponent
  • }
  • ],
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <p>
  • <a (click)="message = 'hello world';">Use message one</a>.
  • <br />
  • <a (click)="message = 'what it be like';">Use message two</a>.
  • </p>
  •  
  • <p>
  • Pipe output: <strong>{{ message | fn:formatMessage }}</strong>
  • </p>
  • `
  • })
  • export class AppComponent {
  •  
  • public message: string;
  •  
  • // I initialize the app-component.
  • constructor() {
  •  
  • this.message = "";
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I am the function being invoked by the FnPipe.
  • public formatMessage( value: string ) : string {
  •  
  • // We can use the "this" reference here because we are providing the AppComponent
  • // as the FnPipeContext token to the local Injector. Its existence will get the
  • // FnPipe to execute the function reference in the current component context.
  • // --
  • // NOTE: As always, we could have used the Fat-Arrow notation (=>) to bind this
  • // function to the AppComponent instance, which would obviate the need for the
  • // FnPipeContext token.
  • return( `Context[ ${ this.constructor.name } ] => ${ value }` );
  •  
  • }
  •  
  • }

As you can see in the AppComponent meta-data, the "viewProviders" are telling the Component's local injector to use an instance of AppComponent as the injectable value for FnPipeContext. Now, when we pass "message" through to the fn reference - formatMessage - the FnPipe transform() method will use the injected AppComponent instance as the execution context.

If we then run this code and click on one of the links, we get the following browser output:


 
 
 

 
 Injecting component references into a Pipe in Angular 6.0.0. 
 
 
 

As you can see, the FnPipe was able to invoke the formatMessage() naked Function reference in the context of the AppComponent through the use of the injected FnPipeContext token.

This works. But, it feels so heavy-handed. If the Pipe could somehow implicitly get access to the context component, that would be a totally different story. But, having to set up the ViewProviders meta-data is ugly. As such, I'd probably just go with the Fat-Arrow approach if the Function needs to be bound to the Component class.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Hey Ben,

Just came across this when wanting to try something similar. In the end I discovered that you can inject an instance of the ChangeDetectorRef and if you inspect it at run-time, you will see an instance of "ViewRef_" which contains a property "context" which is the instance of the component in which the pipe is being used.

This would allow you to bind to the component instance without any special providers being set up. On the downside it's a bit hacky because the ChangeDetectorRef type itself does not have the "context" property.

Reply to this Comment

@Michael,

Hmm, sounds very interesting. Though, the fact that it has a _ in the name makes me think that you're accessing some sort of "private" field. Not that's "wrong", per-say. But, it could be brittle, if the internal implementation breaks.

Just thinking out-loud. Thanks for bringing this to my attention :)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.