Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 how to keep event from triggering digest loop/detection cycle?

We are implementing drag and drop functionality with Angular 2.

I'm using the dragover event just to run the preventDefault() function. So that the drop event works as explained in this question.

The dragover method is being handled by the onDragOver function in the component.

<div draggable="true"
    (dragover)="onDragOver($event)">
...

In the component, this function prevents default behavior allowing for the dragged item to be dropped at this target.

onDragOver(event) {
    event.preventDefault();
}

This works as expected. The dragover event gets fired every few hundred milliseconds.

But, every time the onDragOver function is called, Angular 2 runs its digest cycle. This slows down the application. I'd like to run this function without triggering the digest cycle.

A workaround we use for this is subscribing to element event and running it outside of the Angular 2's context as follows:

constructor( ele: ElementRef, private ngZone: NgZone ) {
    this.ngZone.runOutsideAngular( () => {
        Observable.fromEvent(ele.nativeElement, "dragover")
            .subscribe( (event: Event) => {
                event.preventDefault();
            }
        );
    });
}

This works fine. But is there a way to achieve this without having to access the nativeElement directly?

like image 667
nipuna777 Avatar asked Mar 30 '17 03:03

nipuna777


2 Answers

1) One interesting solution might be overriding EventManager

custom-event-manager.ts

import { Injectable, Inject, NgZone  } from '@angular/core';
import { EVENT_MANAGER_PLUGINS, EventManager } from '@angular/platform-browser';

@Injectable()
export class CustomEventManager extends EventManager {
  constructor(@Inject(EVENT_MANAGER_PLUGINS) plugins: any[], private zone: NgZone) {
    super(plugins, zone); 
  }

  addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
    if(eventName.endsWith('out-zone')) {
      eventName = eventName.split('.')[0];
      return this.zone.runOutsideAngular(() => 
          super.addEventListener(element, eventName, handler));
    } 

    return super.addEventListener(element, eventName, handler);
  }
}

app.module.ts

  ...
  providers: [
    { provide: EventManager, useClass: CustomEventManager }
  ]
})
export class AppModule {

Usage:

<h1 (click.out-zone)="test()">Click outside ng zone</h1>

<div (dragover.out-zone)="onDragOver($event)">

Plunker Example

So with solution above you can use one of these options to prevent default behavior and run event outside angular zone:

(dragover.out-zone)="$event.preventDefault()"
(dragover.out-zone)="false"
(dragover.out-zone)="!!0"

2) One more solution provided by Rob Wormald is using blacklist for Zonejs

blacklist.ts

/// <reference types='zone.js/dist/zone.js' />

const BLACKLISTED_ZONE_EVENTS: string[] = [
  'addEventListener:mouseenter',
  'addEventListener:mouseleave',
  'addEventListener:mousemove',
  'addEventListener:mouseout',
  'addEventListener:mouseover',
  'addEventListener:mousewheel',
  'addEventListener:scroll',
  'requestAnimationFrame',
];

export const blacklistZone = Zone.current.fork({
  name: 'blacklist',
  onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone,
                   task: Task): Task => {

    // Blacklist scroll, mouse, and request animation frame events.
    if (task.type === 'eventTask' &&
        BLACKLISTED_ZONE_EVENTS.some(
            (name) => task.source.indexOf(name) > -1)) {
      task.cancelScheduleRequest();

      // Schedule task in root zone, note Zone.root != target,
      // "target" Zone is Angular. Scheduling a task within Zone.root will
      // prevent the infinite digest cycle from appearing.
      return Zone.root.scheduleTask(task);
    } else {
      return delegate.scheduleTask(target, task);
    }
  }
});

main.ts

import {blacklistZone} from './blacklist'

blacklistZone.run(() => {
  platformBrowser().bootstrapModuleFactory(...)
})

Plunker with blacklist

Update:

5.0.0-beta.7 (2017-09-13)

fix(platform-browser): run BLACK_LISTED_EVENTS outside of ngZone

Follow

  • https://github.com/angular/angular/issues/19989

  • https://github.com/angular/angular/commit/e82812b9a9992bc275bba54575f9d780753c1f8f

  • https://github.com/angular/angular/pull/21681

Update 2

Angular cli includes template for disabling parts of macroTask/DomEvents patch.

Just open

polyfills.ts

You can find there the following code

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 */

 // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
 /*
 * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
 * with the following flag, it will bypass `zone.js` patch for IE/Edge
 */
// (window as any).__Zone_enable_cross_context_check = true;

https://github.com/angular/devkit/blob/8651a94380eccef0e77b509ee9d2fff4030fbfc2/packages/schematics/angular/application/files/sourcedir/polyfills.ts#L55-L68

See also:

  • https://angular.io/guide/browser-support#polyfills-for-non-cli-users
like image 104
yurzui Avatar answered Nov 09 '22 05:11

yurzui


You can detach the change detector to prevent change detection to be invoked for a component

constructor(private cdRef:ChangeDetectorRef) {}
foo() {
  this.cdRef.detach();
  ...
  this.cdRef.attach();
}
like image 9
Günter Zöchbauer Avatar answered Nov 09 '22 04:11

Günter Zöchbauer