Zone detects any async opreations. Once an async oprations happens in Angular, Zone will notify change detection to kick in.
Images we have 5000 svg box displaying on the screen.
And each svg elements and three event listener on it:
@Component({ ... template: ` <svg (mousedown)="mouseDown($event)" (mouseup)="mouseUp($event)" (mousemove)="mouseMove($event)"> <svg:g box *ngFor="let box of boxes" [box]="box"> </svg:g> </svg> ` }) class AppComponent { ... }
Three (3) event handlers are bound to the outer SVG element. When any of these events fire and their handlers have been executed then change detection is performed. In fact, this means that Angular will run change detection, even when we just move the mouse over the boxes without actually dragging a single box!
Since 'mousemove' is the event cause the change detection which is not necessary. So we simple remove it from the HTML:
<svg (mousedown)="mouseDown($event)"
(mouseup)="mouseUp($event)">
<svg:g box *ngFor="let box of boxes" [box]="box">
</svg:g>
</svg>
So now we need a way to attach 'mousemove' event for only the selected svg box. We can do this inside 'mousedown' event:
constructor(private zone: NgZone) {} mouseDown(event) { ... this.element = event.target; this.zone.runOutsideAngular(() => { window.document.addEventListener('mousemove', this.mouseMove.bind(this)); }); }
We inject NgZone
and call runOutsideAngular()
inside our mouseDown()
event handler, in which we attach an event handler for the mousemove
event. This ensures that the mousemove
event handler is really only attached to the document when a box is being selected.
mouseMove(event) { event.preventDefault(); this.element.setAttribute('x', event.clientX + this.clientX + 'px'); this.element.setAttribute('y', event.clientX + this.clientY + 'px'); }
In addition, we save a reference to the underlying DOM element of the clicked box so we can update its x
and y
attributes in the mouseMove()
method. We’re working with the DOM element instead of a box object with bindings for x
and y
, because bindings won’t be change detected since we’re running the code outside Angular’s Zone. In other words, we do update the DOM, so we can see the box is moving, but we aren’t actually updating the box model (yet).
In the next step, we want to make sure that, whenever we release a box (mouseUp
), we update the box model, plus, we want to perform change detection so that the model is in sync with the view again. The cool thing about NgZone
is not only that it allows us to run code outside Angular’s Zone, it also comes with APIs to run code inside the Angular Zone, which ultimately will cause Angular to perform change detection again. All we have to do is to call NgZone.run()
and give it the code that should be executed.
Here’s the our updated mouseUp()
event handler:
@Component(...) export class AppComponent { ... mouseUp(event) { // Run this code inside Angular's Zone and perform change detection this.zone.run(() => { this.updateBox(this.currentId, event.clientX + this.offsetX, event.clientY + this.offsetY); this.currentId = null; }); window.document.removeEventListener('mousemove', this.mouseMove); } }
Also notice that we’re removing the event listener for the mousemove
event on every mouseUp. Otherwise, the event handler would still be executed on every mouse move. In other words, the box would keep moving even after the finger was lifted, essentially taking the drop part out of drag and drop. In addition to that, we would pile up event handlers, which could not only cause weird side effects but also blows up our runtime memory.
Notice:
However, we shouldn’t forget that this solution also comes with a couple (probably fixable) downsides. For example, we’re relying on DOM APIs and the global window
object, which is something we should always try to avoid. If we wanted to use this code with on the server-side then direct access of the window variable would be problematic. We will discus these server-side specific issues in a future article. For the sake of this demo, this isn’t a big deal though.