9

I'm encountering a simple problem which has a hacky solution of setTimeout(...,0).

Looking at this simple code :

@Component({
  selector: 'my-app',
  template: `
    <div>
      <input value='Fill Data' type='button' (click)='fill()'/>
      <span *ngFor="let o of Items" class='mySpan'>Span To Detect<br></span>
    </div>
  `,
})
export class App {
  Items:Array<number> = new Array<number>();

  fill()
  {
   this.Items = [1,2,3,4,5,6,7,8,9,10]
   this.analyzeDom(); //this has to run here
  }

  analyzeDom()
   {
      alert($("div .mySpan").length) // "0"

     //BUT if I set this hacky trick , it works
     // setTimeout(function (){  alert($("div .mySpan").length)},0) // "10"
   }
}

If I click the button , the alert shows "0". I understand why it's happening. It's becuase Angular didn't complete its cycle to actually populate the ngFor.

However - doing this trick with setTimeout(..,0) seems a bit hacky to me and I prefer not to trust on it.

Question:

What is the right way to "wait for operation" in Angular ? (so that I'll see "10") ?

Plnkr

Julien TASSIN
  • 5,004
  • 1
  • 25
  • 40
Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • By using [lifecycle hooks](https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html) - `ngOnInit` or the different `ngDoCheck` methods – mhodges Apr 17 '17 at 18:29
  • @mhodges But it's on demand by clicking the button, and this button can be clicked much after those hooks have already occured – Royi Namir Apr 17 '17 at 18:31
  • What is the reason you want to make it synchronous and delay function until DOM would be updated? Isn't better to read length from current model istead of DOM like `alert(this.Items.length)` ? – Kuba Apr 17 '17 at 18:42
  • @Kuba Nope , becuase i'm checking each element at scroll : "`IsInViewPort(elemenet)`" so the Array itself doesn't help much. – Royi Namir Apr 17 '17 at 18:43
  • 1
    @yurzui you should really provide them as answers so we can upvote : p – eko Apr 17 '17 at 18:49
  • See answer - http://stackoverflow.com/a/35826628/3696510 – muttonUp Apr 17 '17 at 18:51

1 Answers1

19

1) You can force Angular to update the DOM by calling cdRef.detectChanges

constructor(private cdRef: ChangeDetectorRef) {}

analyzeDom(){
  this.cdRef.detectChanges();
  alert($("div .mySpan").length)

Plunker

As setTimeout is macrotask therefore it is running next digest cycle. It means that after calling setTimeout all components tree will be checked

cdRef.detectChanges doesn't call appRef.tick(). It only executes change detection component itself and its children.

2) or you can wait until Angulat has updated DOM by subscribing to zone.onMicrotaskEmpty

import 'rxjs/add/operator/first';

constructor(private zone: NgZone) {}
...
this.zone.onMicrotaskEmpty.first().subscribe(() => this.analyzeDom());

Note: first operator can cause memory leak. See https://github.com/angular/material2/issues/6905

Subscribing to onMicrotaskEmpty doesn't call change detection cycle

Plunker

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Can you please explain how differ `setTimeout(...0)` is as opposed to your solutions ?? – Royi Namir Apr 17 '17 at 19:01
  • 1
    @Royi setTimeout works because it bumps the function you pass to it into the next evaluation cycle. So in your code you make your changes, then Angular detects those changes and alters the DOM accordingly. After that pass through the code, your timeout function executes. Really isn't that bad a solution except that it's not an Angular solution and therefore hacky. The solutions listed above explicitly say execute my code after the dom has been updated. – SethWhite Apr 17 '17 at 19:10
  • @SethWhite Thank for the clarification , however , I'm searching for a good article about `onMicrotaskEmpty ` ( for learning purpose) - still haven't found any – Royi Namir Apr 17 '17 at 19:12
  • @yuruzi - Thank you . (like always). – Royi Namir Apr 17 '17 at 19:12
  • Did you import `import 'rxjs/add/operator/first';`? – yurzui Apr 17 '17 at 19:21