Skip to content

fix(drag-drop): error if drag item is destroyed before the zone has stabilized #13978

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/cdk/drag-drop/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,14 @@ describe('CdkDrag', () => {
expect(event.stopPropagation).toHaveBeenCalled();
}));

it('should not throw if destroyed before the first change detection run', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);

expect(() => {
fixture.destroy();
}).not.toThrow();
}));

});

describe('draggable with a handle', () => {
Expand Down
47 changes: 28 additions & 19 deletions src/cdk/drag-drop/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
/** Subscription to the event that is dispatched when the user lifts their pointer. */
private _pointerUpSubscription = Subscription.EMPTY;

/** Subscription to the stream that initializes the root element. */
private _rootElementInitSubscription = Subscription.EMPTY;

/** Elements that can be used to drag the draggable item. */
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;

Expand Down Expand Up @@ -266,30 +269,36 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
// element to be in the proper place in the DOM. This is mostly relevant
// for draggable elements inside portals since they get stamped out in
// their original DOM position and then they get transferred to the portal.
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
const rootElement = this._rootElement = this._getRootElement();
rootElement.addEventListener('mousedown', this._pointerDown, passiveEventListenerOptions);
rootElement.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
toggleNativeDragInteractions(rootElement , false);
});
this._rootElementInitSubscription = this._ngZone.onStable.asObservable()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could to use takeUntil with _destroyed subject here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but then we'd have the _destroyed only for this one case.

.pipe(take(1))
.subscribe(() => {
const rootElement = this._rootElement = this._getRootElement();
rootElement.addEventListener('mousedown', this._pointerDown, passiveEventListenerOptions);
rootElement.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
toggleNativeDragInteractions(rootElement , false);
});
}

ngOnDestroy() {
this._rootElement.removeEventListener('mousedown', this._pointerDown,
passiveEventListenerOptions);
this._rootElement.removeEventListener('touchstart', this._pointerDown,
passiveEventListenerOptions);
this._destroyPreview();
this._destroyPlaceholder();

// Do this check before removing from the registry since it'll
// stop being considered as dragged once it is removed.
if (this._isDragging()) {
// Since we move out the element to the end of the body while it's being
// dragged, we have to make sure that it's removed if it gets destroyed.
this._removeElement(this._rootElement);
// The directive might have been destroyed before the root element is initialized.
if (this._rootElement) {
this._rootElement.removeEventListener('mousedown', this._pointerDown,
passiveEventListenerOptions);
this._rootElement.removeEventListener('touchstart', this._pointerDown,
passiveEventListenerOptions);

// Do this check before removing from the registry since it'll
// stop being considered as dragged once it is removed.
if (this._isDragging()) {
// Since we move out the element to the end of the body while it's being
// dragged, we have to make sure that it's removed if it gets destroyed.
this._removeElement(this._rootElement);
}
}

this._rootElementInitSubscription.unsubscribe();
this._destroyPreview();
this._destroyPlaceholder();
this._nextSibling = null;
this._dragDropRegistry.removeDragItem(this);
this._removeSubscriptions();
Expand Down