Dale Harders Dale Harders - 3 months ago 26
Javascript Question

Is there a way to prevent DOM removal in Angular 2 RC5?

I'm having issues with Angular 2 and touch devices. In particular, when a Component is rendered via NgFor and is (touch) dragged about the screen. The issue arises if a re-render of the NgFor occurs during a touch drag (due to an external event updating the data bound to the NgFor, which is common in my app). The

touchmove
events stop firing, and require you to lift your finger and place it back down again, which is a terrible mobile experience. This issue does not occur if you use a mouse.

Essentially, in my app I listen for the
touchstart
event on my component, show another 'DragComponent' via a conditional
*ngIf="isDragging"
(which is not within the NgFor) and it is moved about the screen based on the
touchmove
event position data.

I know why this happens. It's due to the browser implementation of the Touch Spec. I normally code around this issue in vanilla js via keeping the DOM element in memory until the
touchend
or
touchcancel
event fire. However, Angular now controls the DOM! And they are removing the element while it's still in use!

Check out this plunker http://plnkr.co/edit/QR6WDzv6NxOmn6LXTngG?p=preview to get more of an understanding of what I'm trying to describe. (Note touchscreen required, or use Touch emulation in Chrome DevTools)

I've also created an issue #9864 in the Angular repo, but haven't had any response. I understand they are busy getting ready for final, but in my opinion this should be solved before final as a lot of users will use Angular on touch devices.

I'd appreciate any tips/workarounds/hacks. Feel free to update the plunker with a solution.

Answer

Found a workaround:

TouchEvents do actually continue to fire after DOM removal BUT they are only targeted at the node/element that the original touchstart occurred on and don't bubble (unlike MouseEvents, which is confusing!).

So, we cant perform a simple @HostListener('touchmove', ['$event']) and expect it to work with DOM removal (as the event listener is attached to the outer component element). We have to dynamically add event listeners to the target element of the touchstart event as they occur. Then perform cleanup on touchend or touchcancel (or ngOnDestroy()).

Soln:

@HostListener('touchstart', ['$event'])
@HostListener('mousedown', ['$event'])
  dragStart(event) {
    if (event.touches) {    // avoid touch event loss issue
      this.removePreviousTouchListeners();    // avoid mem leaks      
      this.touchmoveListenFunc = this.renderer.listen(event.target, 'touchmove', (e) => { this.onDragMove(e); });
      this.touchendListenFunc = this.renderer.listen(event.target, 'touchend', (e) => { this.removePreviousTouchListeners(); this.onDragEnd(e); });
      this.touchcancelListenFunc = this.renderer.listen(event.target, 'touchcancel', (e) => { this.removePreviousTouchListeners(); this.onDragEnd(e); });
    }
   ...
}

removePreviousTouchListeners() {
    if (this.touchmoveListenFunc !== null)
      this.touchmoveListenFunc();             // remove previous listener
    if (this.touchendListenFunc !== null)
      this.touchendListenFunc();              // remove previous listener
    if (this.touchcancelListenFunc !== null)
      this.touchcancelListenFunc();           // remove previous listener

    this.touchmoveListenFunc = null;
    this.touchendListenFunc = null;
    this.touchcancelListenFunc = null;
  }

 @HostListener('mousemove', ['$event'])
  // @HostListener('touchmove', ['$event'])    // don't declare this, as it is added dynamically
  onDragMove(event) {
    ...   // do stuff with event
  }

@HostListener('mouseup', ['$event'])
  // @HostListener('touchend', ['$event'])     // don't use these as they are added dynamically
  // @HostListener('touchcancel', ['$event']) // don't use these as they are added dynamically
  onDragEnd(event) {
    ...  // do stuff
  }

 ngOnDestroy() {
   this.removePreviousTouchListeners();

Don't forget to inject Renderer in the constructor (import from @angular/core

Source https://plus.google.com/+RickByers/posts/GHwpqnAFATf

Comments