3
votes

Using Angular 4.3 and the following Plunkr.

Please consider the following components:

@Component({
  selector: 'my-app',
  template: `
    <div>
      <button type="button" (click)="toggle()">Toggle</button>
      <div #anchor></div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {
  @ViewChild('anchor', {read: ViewContainerRef}) anchor: ViewContainerRef;
  dynamicRef: ComponentRef;
  value = true;

  constructor(private cfr: ComponentFactoryResolver, private cdr: ChangeDetectorRef) {}

  ngAfterViewInit(): void {
    let factory = this.cfr.resolveComponentFactory(Dynamic);
    this.dynamicRef = this.anchor.createComponent(factory);
    this.dynamicRef.instance.value = this.value;
    this.dynamicRef.changeDetectorRef.detectChanges();
  }

  toggle(): void {
    this.value = !this.value;
    this.dynamicRef.instance.value = this.value;
    this.dynamicRef.changeDetectorRef.detectChanges();
  }
}

@Component({
  template: `Value: {{value}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Dynamic {
  @Input() value = true;
}

The App component creates the Dynamic component using the standard ComponentFactoryResolver + ViewContainerRef strategy. Both components have an OnPush change detection strategy. When the App.toggle() method is called, it toggles App.value, propagates this new value to Dynamic.value and forces a change detection run on the dynamic component's change detector. I expect the correct value to be displayed by the dynamic component's template, but it actually never changes. Switching both components to the Default change detection strategy provides the expected behaviour.

Why isn't the dynamic component template re-rendered properly and how can this be fixed?

1
Probably unrelated: class App is missing AfterViewInit implementation - Vega
Not sure what you mean, I do see App.ngAfterViewInit() {...} in both the Plunkr and this post? - Spiff
export class App implements AfterViewInit angular.io/api/core/AfterViewInit#how-to-use - Vega
Yes the lifecycle interface hasn't been added to the class declaration. I think it's unrelated to this issue though. - Spiff
That's what I said :) - Vega

1 Answers

13
votes

Every dynamically created component has a host view. So you have the following hierarchy:

AppComponentView
   DynamicComponentHostView
       DynamicComponentView

When you do:

this.dynamicRef.changeDetectorRef

you get changeDetectorRef of the DynamicComponentHostView. And when you run detectChanges you run change detection for the DynamicComponentHostView, not DynamicComponentView.

Then, if you set OnPush strategy the parent component updates bindings of the child component and decides whether to run change detection on the child component or not. However, it's important, that the input bindings be defined during compilation in the template. Which is not the case with dynamic components. So you have to trigger change detection manually on this component if you want to use OnPush. To do that, you need to get the change detector of the DynamicComponentView. You can do it like this:

export class Dynamic {
  @Input() value = true;

  constructor(public cd: ChangeDetectorRef) {

  }

And then trigger change detection like this:

this.dynamicRef.instance.cd.detectChanges();

Here is the plunker.

For more information about change detection read Everything you need to know about change detection in Angular.