Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug(drag-drop): Root element pointer down listeners causing excessive change detection #18726

Closed
Achilles1515 opened this issue Mar 5, 2020 · 5 comments · Fixed by #18821, lingounet/testage#29 or michael-vasyliv/ngx-virtual-swiper#24
Assignees
Labels
P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent

Comments

@Achilles1515
Copy link

Achilles1515 commented Mar 5, 2020

Reproduction

https://stackblitz.com/edit/angular-kwisjn?file=src/app/cdk-drag-drop-overview-example.html

The StackBlitz demo above is a slightly modified version of the "Basic Drag&Drop" official example with a change detection counter added and a second draggable box.

Simply clicking (and not even dragging) an element containing the cdkDrag directive causes app-wide change detection to occur. This behavior is exemplified by clicking the second box and witnessing the change detection counter increase.

The first draggable box in the demo has had its root element listeners (mousedown and touchstart) modified to run outsize Angular zone.

drag-ref.ts code:

/**
   * Sets an alternate drag root element. The root element is the element that will be moved as
   * the user is dragging. Passing an alternate root element is useful when trying to enable
   * dragging on an element that you might not have access to.
   */
  withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this {
    const element = coerceElement(rootElement);

    if (element !== this._rootElement) {
      if (this._rootElement) {
        this._removeRootElementListeners(this._rootElement);
      }

      element.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
      element.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
      this._initialTransform = undefined;
      this._rootElement = element;
    }

    return this;
  }

Is there any reason these event listeners can't be instantiated outside the Angular zone? Like so:

  withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this {
    const element = coerceElement(rootElement);

    if (element !== this._rootElement) {
      if (this._rootElement) {
        this._removeRootElementListeners(this._rootElement);
      }

      this._ngZone.runOutsideAngular(() => {
        element.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
        element.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
      });
      this._initialTransform = undefined;
      this._rootElement = element;
    }

    return this;
  }

Expected Behavior

No change detection occurs when simply clicking a draggable item.

Actual Behavior

Change detection occurs when simply clicking a draggable item.

Environment

  • Angular:
  • CDK/Material: 9.1.1
@Achilles1515 Achilles1515 added the needs triage This issue needs to be triaged by the team label Mar 5, 2020
@crisbeto
Copy link
Member

crisbeto commented Mar 6, 2020

There are some data bindings that need to be updated once dragging starts (e.g. .cdk-drag-dragging), hence why we run the events inside the NgZone.

@Achilles1515
Copy link
Author

There are some data bindings that need to be updated once dragging starts (e.g. .cdk-drag-dragging), hence why we run the events inside the NgZone.

Good to know, I wasn't thinking about host bindings.

But with the current implementation, I still don't see the need to run the pointerdown events inside NgZone. If you look at the StackBlitz demo I posted and watch the DOM elements update in dev tools, the .cdk-drag-dragging class still gets added to both boxes at the same time.

.cdk-drag-dragging won't get added until _hasStartedDragging is true, which happens here:

/** Handler that is invoked when the user moves their pointer after they've initiated a drag. */
  private _pointerMove = (event: MouseEvent | TouchEvent) => {
    // ...

    if (!this._hasStartedDragging) {
     //...
      if (isOverThreshold) {
        //...
        if (!this._dropContainer || !this._dropContainer.isDragging()) {
          this._hasStartedDragging = true;
          this._ngZone.run(() => this._startDragSequence(event));
        }
      }

      return;
    }
//...
}

Immediately following this._hasStartedDragging = true;, _startDragSequence emits the started event, setting the directive host view and its ancestors as dirty, and the _ngZone.run() above will trigger change detection and update the bindings.

private _handleEvents(ref: DragRef<CdkDrag<T>>) {
    ref.started.subscribe(() => {
      this.started.emit({source: this});

      // Since all of these events run outside of change detection,
      // we need to ensure that everything is marked correctly.
      this._changeDetectorRef.markForCheck();
    });
//...
}

Does anything else come to mind that relies on the pointerdown events being run insize NgZone?

@crisbeto
Copy link
Member

Looking into it deeper, you're right that it's not necessary. I think this is a leftover from when dragging didn't have a threshold so it had to trigger change detection immediately.

@crisbeto crisbeto added has pr P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent and removed needs triage This issue needs to be triaged by the team labels Mar 13, 2020
@crisbeto crisbeto self-assigned this Mar 13, 2020
crisbeto added a commit to crisbeto/material2 that referenced this issue Mar 13, 2020
…vents

When the drag&drop was initially implemented it had to trigger change detection immediately once dragging started so that the data bindings get updated correctly. After some time we introduced a threshold for dragging that also triggers its own change detection and makes the initial change detection unnecessary.

Fixes angular#18726.
@Achilles1515
Copy link
Author

Looking into it deeper, you're right that it's not necessary. I think this is a leftover from when dragging didn't have a threshold so it had to trigger change detection immediately.

Great. Thanks for looking into it.

jelbourn pushed a commit that referenced this issue Mar 31, 2020
…vents (#18821)

When the drag&drop was initially implemented it had to trigger change detection immediately once dragging started so that the data bindings get updated correctly. After some time we introduced a threshold for dragging that also triggers its own change detection and makes the initial change detection unnecessary.

Fixes #18726.
jelbourn pushed a commit that referenced this issue Apr 6, 2020
…vents (#18821)

When the drag&drop was initially implemented it had to trigger change detection immediately once dragging started so that the data bindings get updated correctly. After some time we introduced a threshold for dragging that also triggers its own change detection and makes the initial change detection unnecessary.

Fixes #18726.

(cherry picked from commit 24a7c6d)
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 1, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Projects
None yet
2 participants