Akxe
Akxe

Reputation: 11575

createEmbeddedView creates view outside of changeDetection

I have a template and set of context to populate templates with. Done this a few times now but after an unknown change (that I am not able to backtrack...), change detection stopped working out of the box.

I do not know what to do whit this I have tried multiple common fixes, but to no avail:

Please, do anyone have more info

@Injectable()
export class MyService {
  createClusterOnMap$<T>(
    contexts$: Observable<readonly T[]>,
    // <ng-template let-data>
    //   <carrier-marker [cluster]="data.cluster" [marker]="data.marker"></carrier-marker>
    // </ng-template>
    clusterTemplate$: Observable<TemplateRef<T>>,
    destroy$: Observable<void>,
  ) {
    combineLatest([
      contexts$,
      clusterTemplate$,
    ]).pipe(
      map(([contexts, template]) => {
        return contexts.forEach(context => {
          const view = template.createEmbeddedView(context);
          
          // I have tried this, but it did not help...
          // this.zone.runGuarded(() => template.createEmbeddedView(context));

          // This will make sure that `ngOnInit` runs,
          // but no further detections are registred
          view.detectChanges();
          // No visible effect
          view.reattach();

          return view;
        });
      }),
    ).subscribe();
  }
}
@Component({ ... })
export class CarrierMarkerComponent implements OnInit {
  @Input() marker!: IHTMLMarker;
  @Input() cluster!: AbstractMapClusterContext<Carrier>;
  
  constructor() {
    // This gets called
    interval(1000).subscribe(() => {
      console.log(this.marker, this.cluster); // undefined, undefined
    });
  }

  ngOnInit(): void {
    // Only gets called if `view.detectChanges();` is called
  }
}

With view.detectChanges() in place, the ngOnInit is called, template is populated, but not even the async pipe inside CarrierMarkerComponent template does not get updated (for a second time), even though the observable fires as it should.

Upvotes: 1

Views: 726

Answers (1)

Alberto Chiesa
Alberto Chiesa

Reputation: 7360

I'm very, very late to this question, but given it's an interesting case, I'm going to describe the probable reason this not worked.

When you find yourself having a set of components "outside" the change detection, and no event triggers change detection afterwards, it means that the original async event that triggered the create did not run in the context of the main Angular Zone.

There is a lot of material available, describing how Zone.JS gives Angular a mechanism for triggering change detection, and I will not repeat it here (see here for example), but sometimes you could find yourself outside the main Zone.

A common case is when using unpatched native API (Websocket messages received, server sent events, or other). You need to "get back into the zone", so the view gets created inside the main Zone:

@Injectable()
export class MyService {
  createClusterOnMap$<T>(
    contexts$: Observable<readonly T[]>,
    clusterTemplate$: Observable<TemplateRef<T>>,
    destroy$: Observable<void>,
  ) {
    combineLatest([
      contexts$,
      clusterTemplate$,
    ]).subscribe(([contexts, template]) => {
        // NgZone.run allows to run code inside the context of the main Angular zone,
        // and will get applied to every callback attached to components created by the callback.
        this.zone.run(() => {
          for(const ctx of contexts) {
            const view = template.createEmbeddedView(ctx);
          }
        });
    });
  }
}

You can get a reference to NgZone having it injected in the constructor.

You can debug what's happening looking into the window.Zone.current property and checking it's the Angular Zone:

Angular zone

...and not the <root> one:

Root zone

from within the offending code.

Upvotes: 1

Related Questions