Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular template communication one tick delayed

I've been playing with ng-template and found myself struggling with some kind of delay.

I created one simple example in stackblitz. https://stackblitz.com/edit/angular-template-series-v?file=app%2Fparent%2Fparent1.component.ts

A Parent components sends a Hello component to its Child through templating.

The idea was to control the template from the Child using an ngIf.

When the Hello component reachs the ngOnInit hook cycle it emits an output. The parent component grabs this output and add a message to display.

But the message isn't displayed when the component is created rather that one cycle later. In the example you need to click twice the button "Toggle content" in order to make the message "Template created" to appear.

How can I address this in order to make the message appear in the same cycle?

like image 777
Adrian Abreu Avatar asked Mar 28 '18 11:03

Adrian Abreu


2 Answers

It's due to how change detection works in Angular. It is not linked to using ng-template, as this modified stackblitz show (using grand child without using ng-template).

2 things to know about change detection:

  • it is triggered after events (click, submit,..), xhr requests or timers.
  • it occurs from top to bottom of the component tree

Basically, when you click the toggle button:

  • the handler is triggered (toggling showContent's value in your example, but nothing else. The hello component is NOT created at this stage)
  • change detection kicks in, always starting from top to bottom.
  • angular starts with checking if the view needs to be updated for the parent component. As nothing has changed yet, the parent view is not updated.
  • The child component then detects that its showContentValue has changed. This causes the creation of the hello component.
  • After initialisation of the hello component, the created event is emitted. The parent component received this event and updates the array of messages accordingly. But the parent's content has aleready been checked at this point, so Angular will make no modification to the parent view.

When you click the toggle button again, then the same cycle runs again and this time the message pushed in the array in the previous cycle is displayed

Here is a modified stackblitz example with logs for lifecycle hooks

Note

Forcing change detection to run again will solve your problem here, but if you are not communicating directly from child to parent, you'd better use a shared service (based on observables) to broadcast messages between unrelated components

like image 60
David Avatar answered Sep 23 '22 19:09

David


This is because you're breaking the Angular change detection cycle.

Here is what is happening after you click on your first toggle button :

1- You click on the toggle

2- ZoneJS, which has hooks in click events gets notified (Simply put)

3- Zone makes sure the click event is finished

4- Zone will let Angular know that an async event has happened, we must check the component and make sure that the view ( DOM ) is reflecting the model ( the model/the javascript world). So change detection will run, and it will detect all the changes and finds that there are bindings in the template (ng-if).

5- After ngIf becomes true, AND NOTE THAT THIS IS ALL SYNC SO FAR, the ng-container gets created and your templateOutlet gets created and your hello component will be rendered.

NOTE AGAIN : this is all sync.

6 - View has been updated, change detection is now off the hook.

7 - Inside your hello component you fire another event which is the created and that notifies the Parent component and inside your parent component you are mutating the array, and Angular doesn't care about that because there hasn't been a async event, nor have you updated any of the inputs, you've just fired another sync event after change detection was finished his job.

8- the Array is now updated, but the view is not ( in the parent component)

9 - You click again, the same happens and Angular updates the parent view in the first change detection run and you see the array is being reflected in the view.

To prove that I'm right :D :

 onTemplateCreated() {
    console.log('Got the event from chil');
   this.template.push('Template created');
    console.log('this.template',this.template);
  }

You'll see that after click , the console is showing the right array, but the view is not untill you click again.

Now let's swich to my code

 onTemplateCreated() {
    console.log('Got the event from chil');
   this.template = [...this.template,'Template created'];
  }

BEcause I'm not mutating and the refrence is being updated, Angular now has to care about this change, BUT there's still an issue, Angular's change detector was finished and you updated the model withouth running any async event, so we're in trouble, because Angular doesn't like the model to be updated after the change detection is finished,

So you'll be thrown an error that says :

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value

To fix that, first read my other post to understand it, second, you need tell Angular that you know what you're doing and run the change detection manually :

  this._cd.detectChanges

OR , you can make it async :

onTemplateCreated() {
    console.log('Got the event from chil');
   setTimeout(()=>{
      this.template = [...this.template,'Template created'];
   })
  }
like image 21
Milad Avatar answered Sep 25 '22 19:09

Milad