21

From my understanding of runOutsideAngular(), if I need to run something that won't trigger the Angular change detection, I need to use this function. My code is not working, however; when I click the button, the UI is changing and the number is 2.

@Component({selector: 'my-cmp', 
template: `<h1>{{num}}</h1>
           <button (click)="onClick()">Change number</button>`})

class MyComponent implements OnChanges {

  num = 1; 
  constructor(private _ngZone: NgZone ) {

  }

  onClick() {
    this._ngZone.runOutsideAngular(() => {
      this.num = 2;
    }}));
  }
}
Ry-
  • 218,210
  • 55
  • 464
  • 476

6 Answers6

27

If anything is causing change detection, and a bound event like (click)="onClick()" does cause change detection, then Angular will detect the change.

runOutsideAngular doesn't mean Angular won't see the change, it only means that the code run this way doesn't cause change detection, but because the click event already does, it's meaningless in your example.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    So how can I run code on click without running the change detection at all? –  Oct 28 '16 at 08:07
  • I don't know how to invoke it from the UI without causing change detection to run. What are you trying to accomplish? – Günter Zöchbauer Oct 28 '16 at 08:08
  • Like with angular 1 I want to create clicks that not trigger the $digest loop –  Oct 28 '16 at 08:09
  • Sure, but for what purpose. I don't know Ng1 and have no experience where this might be useful. Perhaps you can wrap the button in a component, prevent bubbling of the event and call the code from there, then change detection might be local to the button component. – Günter Zöchbauer Oct 28 '16 at 08:30
10

[In short] you need to change one line in your current code

onClick() {
    this._ngZone.runOutsideAngular(() => {
        setTimeout(()=>this.num = 2,0); // instead of this.num = 2;
    }}));
  }

now if you click the on the <button>, this.num will become 2, but you won't see any change in the UI (a temporary inconsistency between view and model)

[Explanation] without runOutsideAngular(), async functions like addEventListener() or setTimeout() behaves differently (monkey patched). their callbacks will try to update UI with Angular after running user's code. For example, you can treat (click)="onClick()" as:

addEventListener("click",function modifiedCallback(){
    onClick();
    updateUIifModelChanges(); //call to Angular
})

In order to not triggered UI update we need to satisfy the following two conditions:

  1. not modify model in function onClick (so, modify inside setTimeout())
  2. when the model is indeed modified, do not invoke updateUIifModelChanges (call setTimeout() inside runOutsideAngular)

[More] of cause, the explanation I gave is a very very...simplified version of what happens. setTimeout() has the same function signature whether it's running inside runOutsideAngular() or not. The reason that it behaves differently is because it's running in a different Zone

watashiSHUN
  • 9,684
  • 4
  • 36
  • 44
7

If you want to prevent change detection then you can

1) subscribe on ngZone.onMicrotaskEmpty like this:

import { NgZone, ChangeDetectorRef } from '@angular/core';
import 'rxjs/add/operator/first';

...
export class MyComponent {
  constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {}

  onClick() {
    // to do something

    this.cdRef.detach();
    this.ngZone.onMicrotaskEmpty.first().subscribe(() => {
      // reattach changeDetector after application.tick()
      this.cdRef.reattach();
    });
  }
}

This handler will run after Application.tick

See also Plunker Example

2) use custom directive like this:

@Directive({
  selector: '[outSideEventHandler]'
})
class OutSideEventHandlerDirective {
  private handler: Function;

  @Input() event: string = 'click'; // pass desired event
  @Output('outSideEventHandler') emitter = new EventEmitter();

  constructor(private ngZone: NgZone, private elRef: ElementRef) {}

  ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
      this.handler = $event => this.emitter.emit($event);
      this.elRef.nativeElement.addEventListener(this.event, this.handler);
    });
  }

  ngOnDestory() {
    this.elRef.nativeElement.removeEventListener(this.event, this.handler);
  }
}

and then in template you can write:

<button (outSideEventHandler)="onClick()">Click outside zone</button>

or

<button event="mousedown" (outSideEventHandler)="onClick()">Click outside zone</button>

Plunker

3) write custom DOM event handler as described in this article.

Other solutions see here:

yurzui
  • 205,937
  • 32
  • 433
  • 399
5

Using ngZone.run is a bit better than the setTimeout solutions since it uses angular specific functionality. Run is meant to be used within ngZone.runOutsideAngular functions.

From the docs:

Running functions via run allows you to reenter Angular zone from a task that was executed outside of the Angular zone (typically started via {@link #runOutsideAngular}).

This is actually a very practical example of say a button that increments a number by one but only triggers change detection when the number is even.

    @Component({selector: 'my-cmp', 
    template: `<h1>{{num}}</h1>
               <button (click)="onClick()">Change number</button>`})

    class MyComponent implements OnChanges {

      num = 1; 
      constructor(private _ngZone: NgZone ) {

      }

      onClick() {
        this._ngZone.runOutsideAngular(() => {
          if(this.num % 2 === 0){
              // modifying the state here wont trigger change.
              this.num++;
          } else{
            this._ngZone.run(() => {
                this.num++;
            })
          }
        
        }}));
      }
    }
denixtry
  • 2,928
  • 1
  • 21
  • 19
  • 3
    I don't think this works, because `runOutsideAngular` is *synchronous*. The callback is invoked immediately, i.e. before `onClick` returns. Change detection is always triggered after the `(click)` event, so your code is no different in practice from the code in the OP. That's why the (working) examples call `setTimeout` inside `runOutsideAngular`, because any changes made before `onClick` returns will be detected regardless of what Zone they were made in. – Coderer Oct 02 '20 at 11:05
1
...
constructor(
        private ngZone: NgZone
){
    ngZone.runOutsideAngular(() => {
        setInterval(()=>{
            this.num= new Date().Format('yyyy-MM-dd HH:mm:ss');
        },1000);
    });
}
...
sweetyx
  • 46
  • 1
  • 2
    While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value. – Donald Duck May 27 '17 at 10:59
0

This is how I have tried to check the difference insideAngular and OutsideAngular

constructor(private zone: NgZone) { }
    
        setProgressOutsideAngular() {
            this.zone.runOutsideAngular(() => {
                setInterval(() => { ++this.progress, console.log(this.progress) }, 500)
            })
        }
    
        setProgressInsideAngular() {
            this.zone.run(() => setInterval(() => { ++this.progress, console.log(this.progress) }, 500))            
    
        }