Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen@CFJamesAllen )

ChangeDetectorRef Is A Special Dependency In Angular 2 RC 3

By Ben Nadel on

Most of the time, in Angular 2, a Component and its sibling Directives (on the same host element) all have access to the same dependencies from the same dependency-injector. The ChangeDetectorRef, however, is one dependency that is given special treatment. As it turns out, if the ChangeDetectorRef is required by a sibling Directive, the directive is given the parent component's change detector, not the one provided to the host component.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

I stumbled upon this behavior when I wanted to see if a directive could detach the ChangeDetectorRef of its host component. Of course, since it can't access the host's ChangeDetectorRef, this is not possible. To see this behavior in action, I've put together a small demo in which I have a Component and a sibling directive, both of which require and log the ChangeDetectorRef:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { MyCounterComponent } from "./counter.component";
  • import { TestChangeDetectorDirective } from "./test-change-detector.directive";
  •  
  • @Component({
  • selector: "my-app",
  • directives: [ MyCounterComponent, TestChangeDetectorDirective ],
  • template:
  • `
  • <p>
  • <a (click)="incrementCounter()">Increment counter</a>
  • </p>
  •  
  • <my-counter [count]="counter" testChangeDetector></my-counter>
  • `
  • })
  • export class AppComponent {
  •  
  • // I hold the counter which is being passed into the Counter component(s).
  • public counter: number;
  •  
  •  
  • // I initialize the component.
  • constructor() {
  •  
  • this.counter = 0;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I increment the counter by one.
  • public incrementCounter() : void {
  •  
  • this.counter++;
  •  
  • }
  •  
  • }

As you can see in the view, the MyCounterComponent element also has an attribute directive implemented by TestChangeDetectorDirective. The MyCounter logic is as such:

  • // Import the core angular services.
  • import { ChangeDetectorRef } from "@angular/core";
  • import { Component } from "@angular/core";
  •  
  • @Component({
  • selector: "my-counter",
  • inputs: [ "count" ],
  •  
  • // Here, we are providing a test value to demonstrate that non-ChangeDetectorRef
  • // dependencies can be provided by the component and required by a sibling directive.
  • providers: [
  • {
  • provide: "ProviderTest",
  • useValue: "Provided by Counter Component."
  • }
  • ],
  • template:
  • `
  • Count: {{ count }}
  • `
  • })
  • export class MyCounterComponent {
  •  
  • // I hold the current count. This is an injected property.
  • public count: number;
  •  
  •  
  • // I initialize the component.
  • constructor( changeDetectorRef: ChangeDetectorRef ) {
  •  
  • console.group( "MyCounter Component" );
  • console.log( changeDetectorRef );
  • console.groupEnd();
  •  
  • }
  •  
  • }

Notice that we're injecting the ChangeDetectorRef into the constructor and logging it out. But, we're also providing a test value, "ProviderTest". This latter value will be required by the TestChangeDetectorDirective in order demonstrate that a directive can - generally speaking - access its host component's dependencies.

  • // Import the core angular services.
  • import { ChangeDetectorRef } from "@angular/core";
  • import { Directive } from "@angular/core";
  • import { Inject } from "@angular/core";
  • import { Self } from "@angular/core";
  •  
  • @Directive({
  • selector: "[testChangeDetector]"
  • })
  • export class TestChangeDetectorDirective {
  •  
  • // I initialize the directive.
  • constructor(
  • @Self() changeDetectorRef: ChangeDetectorRef,
  • @Self() @Inject( "ProviderTest" ) providerTest: string
  • ) {
  •  
  • console.group( "TestChangeDetector Directive" );
  • console.log( changeDetectorRef );
  • console.log( providerTest );
  • console.groupEnd();
  •  
  • }
  •  
  • }

As you can see here, we're injecting both the ChangeDetectorRef and the test value provided by the host component. And, when we run this demo, we get the following output:


 
 
 

 
 ChangeDetectorRef dependency is a special dependency in Angular 2 RC 3. 
 
 
 

As you can see, the Component and its sibling Directive received different ChangeDetectorRef instances. The Component receives its own while the sibling Directive is given the one provided by the parent component. This happens because the ChangeDetectorRef is given special treatment by the Angular 2 compiler. Really minor note, but good to keep in the back of your head.




Reader Comments

Interesting find. I imagine the day that I use this to solve a problem is when I have truly mastered the Angular 2 force :)

Reply to this Comment

@Sam,

Ha ha, I am not sure this particular find should be given too much weight :P It was just something that I stumbled over doing some R&D.

Reply to this Comment

@Ben

Don't under estimate :) Great find.

From the top of my head, here's what I think happens:
MyCounterComponent is a component which means it has an AppView and an AppElement, it also attaches to another view.
TestChangeDetectorDirective doesn't have a view.

In the process of creating a child view (i.e: creating MyCounterComponent) you need to supply an injector, this is usually done by taking the parent injector (injector of AppComponent) and instantiate a new child injector with some locals (ChangeDetectorRef for example).

This means that MyCounterComponent gets an new injector with locals matching his view (ViewContainerRef, ChangeDetectorRef etc...).

TestChangeDetectorDirective gets the parent injector so ChangeDetectorRef is different.

I wonder why this is the behaviour, I guess there is a reason.

Reply to this Comment

@Shlomi,

For some background, the thing that got me curious about this was possibly being able to create a directive that would detach its host's ChangeDetection strategy. Imagine having some massive data-grid component that was change intensive. Then, imagine being able to arbitrarily detach it based on the calling context. Something like:

<data-grid [data]="massiveData" renderOnce></data-gird>

In this case, the "renderOnce" would be a directive that would do nothing but require the host ChangeDetectorRef and then .detach() it after the content has been init'ed.

I thought this would be cool because it meant that the change detection strategy didn't have to be opened by the data-grid itself, which make more sense. Much like the "::value" syntax in Angular 1.x.

But alas, no luck :)

Reply to this Comment

You should include more of this reasoning in your posts:

" Imagine having some massive data-grid component that was change intensive. Then, imagine being able to arbitrarily detach it based on the calling context."

I find understanding the problem someone was solving really helps me understand why Angular is designed the way it is. It is certainly obvious to you, but I hadn't considered that use case when reading this post originally.

Just my two cents :)

Reply to this Comment

@Sam,

100% agreed. And to be honest, this post started at as *that* post. Meaning, the post where I was going to create a directive that override the change detection for the host element ... except for the fact that it didn't work because it received the wrong ChangeDetectorRef. So, I had to pivot to the new finding instead of the previous desire ... and I think it got lost in translation, so to speak.

But yeah, definitely with posts like this where its really *just* about the mechanics of Angular, I'll try to be sure to include a "and the reason this might be interesting is ...." kind of thoughts. Thanks for the feedback!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.