17

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?

fscheidl
  • 2,281
  • 3
  • 19
  • 33
nipuna-g
  • 6,252
  • 3
  • 31
  • 48
  • 1
    What do you mean by "digest cycle keeps getting triggered every time that something is dragged over the target" ? You mean like every element which is already inside the div triggers this event? – eko Apr 03 '17 at 05:17
  • @echonax I've updated the quesiton: The dragover event gets fired every few hundred milliseconds as we expect it to. But, every time the `onDragOver` function is called with this, Angular 2 runs its digest cycle. This slows down the application. I'd like to run this function without triggering the digest cycle. – nipuna-g Apr 03 '17 at 05:47

2 Answers2

24

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

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:

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • #2 Really helped me out. Thanks a lot! – instantaphex Nov 06 '17 at 17:42
  • Could you provide a plunker for example 2? Something I'm looking for however its unclear where 'Zone' is imported from or referenced ? – Ashg Feb 07 '18 at 01:08
  • @Ashg I updated answer. You can also include zone types in tsconfig instead of Triple-Slash Directive like https://github.com/angular/angular/blob/master/packages/core/tsconfig-build.json#L16. Or if you don't want to have types then just use `declare let Zone: any` and `any` for such types like `ZoneDelegate`, `Task` etc – yurzui Feb 07 '18 at 05:49
  • 1
    Thanks yurzui for that, I figured that part out eventually, however when I did implement this solution, I still found that expressions were being evaluated on mouse movements, the onScheduleTask was being invoked after the component expression was evaluated which to me defeats the purpose, unless I am missing something. Hence why I thought a working Plunker would be helpful. Thank you anyway. – Ashg Feb 08 '18 at 00:50
  • @Ashg Added plunker. As you can angular won't run change detection on mousemove. Only when you click on Update button – yurzui Feb 08 '18 at 06:16
  • `__zone_symbol__BLACK_LISTED_EVENTS` didn't work for me at all, but the CustomEventManager is perfect! :) My drag'n'drop came from inusable to perfect after `(dragover.out-zone)="..."`. – Bruno Medeiros Jun 13 '18 at 04:29
9

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();
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567