Skip to content

Commit

Permalink
fix(cdk/drag-drop): resolve helper directives with DI for proper host…
Browse files Browse the repository at this point in the history
…Directives support (#28633)

Currently `CdkDrag` resolve its helper directives (e.g. handle or preview) using a content query, but that doesn't work when it's applied as a host directive, because no content is being projected.

These changes switch to having the helper directives inject the closest drag directive and register themselves manually.

Fixes #28614.

(cherry picked from commit ef68e32)
  • Loading branch information
crisbeto committed Feb 26, 2024
1 parent 94eafc1 commit 4af777a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 49 deletions.
9 changes: 4 additions & 5 deletions src/cdk/drag-drop/directives/drag-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
booleanAttribute,
} from '@angular/core';
import {Subject} from 'rxjs';
import type {CdkDrag} from './drag';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {assertElementNode} from './assertions';

Expand All @@ -38,9 +39,6 @@ export const CDK_DRAG_HANDLE = new InjectionToken<CdkDragHandle>('CdkDragHandle'
providers: [{provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle}],
})
export class CdkDragHandle implements OnDestroy {
/** Closest parent draggable instance. */
_parentDrag: {} | undefined;

/** Emits when the state of the handle has changed. */
readonly _stateChanges = new Subject<CdkDragHandle>();

Expand All @@ -57,16 +55,17 @@ export class CdkDragHandle implements OnDestroy {

constructor(
public element: ElementRef<HTMLElement>,
@Inject(CDK_DRAG_PARENT) @Optional() @SkipSelf() parentDrag?: any,
@Inject(CDK_DRAG_PARENT) @Optional() @SkipSelf() private _parentDrag?: CdkDrag,
) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
assertElementNode(element.nativeElement, 'cdkDragHandle');
}

this._parentDrag = parentDrag;
_parentDrag?._addHandle(this);
}

ngOnDestroy() {
this._parentDrag?._removeHandle(this);
this._stateChanges.complete();
}
}
16 changes: 13 additions & 3 deletions src/cdk/drag-drop/directives/drag-placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, TemplateRef, Input, InjectionToken} from '@angular/core';
import {Directive, TemplateRef, Input, InjectionToken, inject, OnDestroy} from '@angular/core';
import {CDK_DRAG_PARENT} from '../drag-parent';

/**
* Injection token that can be used to reference instances of `CdkDragPlaceholder`. It serves as
Expand All @@ -24,8 +25,17 @@ export const CDK_DRAG_PLACEHOLDER = new InjectionToken<CdkDragPlaceholder>('CdkD
standalone: true,
providers: [{provide: CDK_DRAG_PLACEHOLDER, useExisting: CdkDragPlaceholder}],
})
export class CdkDragPlaceholder<T = any> {
export class CdkDragPlaceholder<T = any> implements OnDestroy {
private _drag = inject(CDK_DRAG_PARENT);

/** Context data to be added to the placeholder template instance. */
@Input() data: T;
constructor(public templateRef: TemplateRef<T>) {}

constructor(public templateRef: TemplateRef<T>) {
this._drag._setPlaceholderTemplate(this);
}

ngOnDestroy(): void {
this._drag._resetPlaceholderTemplate(this);
}
}
23 changes: 20 additions & 3 deletions src/cdk/drag-drop/directives/drag-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, InjectionToken, Input, TemplateRef, booleanAttribute} from '@angular/core';
import {
Directive,
InjectionToken,
Input,
OnDestroy,
TemplateRef,
booleanAttribute,
inject,
} from '@angular/core';
import {CDK_DRAG_PARENT} from '../drag-parent';

/**
* Injection token that can be used to reference instances of `CdkDragPreview`. It serves as
Expand All @@ -24,12 +33,20 @@ export const CDK_DRAG_PREVIEW = new InjectionToken<CdkDragPreview>('CdkDragPrevi
standalone: true,
providers: [{provide: CDK_DRAG_PREVIEW, useExisting: CdkDragPreview}],
})
export class CdkDragPreview<T = any> {
export class CdkDragPreview<T = any> implements OnDestroy {
private _drag = inject(CDK_DRAG_PARENT);

/** Context data to be added to the preview template instance. */
@Input() data: T;

/** Whether the preview should preserve the same size as the item that is being dragged. */
@Input({transform: booleanAttribute}) matchSize: boolean = false;

constructor(public templateRef: TemplateRef<T>) {}
constructor(public templateRef: TemplateRef<T>) {
this._drag._setPreviewTemplate(this);
}

ngOnDestroy(): void {
this._drag._resetPreviewTemplate(this);
}
}
77 changes: 50 additions & 27 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {Directionality} from '@angular/cdk/bidi';
import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ContentChild,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
Expand All @@ -21,7 +19,6 @@ import {
OnDestroy,
Optional,
Output,
QueryList,
SkipSelf,
ViewContainerRef,
OnChanges,
Expand All @@ -32,7 +29,7 @@ import {
booleanAttribute,
} from '@angular/core';
import {coerceElement, coerceNumberProperty} from '@angular/cdk/coercion';
import {Observable, Observer, Subject, merge} from 'rxjs';
import {BehaviorSubject, Observable, Observer, Subject, merge} from 'rxjs';
import {startWith, take, map, takeUntil, switchMap, tap} from 'rxjs/operators';
import type {
CdkDragDrop,
Expand All @@ -44,8 +41,8 @@ import type {
CdkDragRelease,
} from '../drag-events';
import {CDK_DRAG_HANDLE, CdkDragHandle} from './drag-handle';
import {CDK_DRAG_PLACEHOLDER, CdkDragPlaceholder} from './drag-placeholder';
import {CDK_DRAG_PREVIEW, CdkDragPreview} from './drag-preview';
import {CdkDragPlaceholder} from './drag-placeholder';
import {CdkDragPreview} from './drag-preview';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, Point, PreviewContainer} from '../drag-ref';
import type {CdkDropList} from './drop-list';
Expand Down Expand Up @@ -77,19 +74,13 @@ export const CDK_DROP_LIST = new InjectionToken<CdkDropList>('CdkDropList');
export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
private readonly _destroyed = new Subject<void>();
private static _dragInstances: CdkDrag[] = [];
private _handles = new BehaviorSubject<CdkDragHandle[]>([]);
private _previewTemplate: CdkDragPreview | null;
private _placeholderTemplate: CdkDragPlaceholder | null;

/** Reference to the underlying drag instance. */
_dragRef: DragRef<CdkDrag<T>>;

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

/** Element that will be used as a template to create the draggable item's preview. */
@ContentChild(CDK_DRAG_PREVIEW) _previewTemplate: CdkDragPreview;

/** Template for placeholder element rendered to show where a draggable would be dropped. */
@ContentChild(CDK_DRAG_PLACEHOLDER) _placeholderTemplate: CdkDragPlaceholder;

/** Arbitrary data to attach to this drag instance. */
@Input('cdkDragData') data: T;

Expand Down Expand Up @@ -351,12 +342,49 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {

// Unnecessary in most cases, but used to avoid extra change detections with `zone-paths-rxjs`.
this._ngZone.runOutsideAngular(() => {
this._handles.complete();
this._destroyed.next();
this._destroyed.complete();
this._dragRef.dispose();
});
}

_addHandle(handle: CdkDragHandle) {
const handles = this._handles.getValue();
handles.push(handle);
this._handles.next(handles);
}

_removeHandle(handle: CdkDragHandle) {
const handles = this._handles.getValue();
const index = handles.indexOf(handle);

if (index > -1) {
handles.splice(index, 1);
this._handles.next(handles);
}
}

_setPreviewTemplate(preview: CdkDragPreview) {
this._previewTemplate = preview;
}

_resetPreviewTemplate(preview: CdkDragPreview) {
if (preview === this._previewTemplate) {
this._previewTemplate = null;
}
}

_setPlaceholderTemplate(placeholder: CdkDragPlaceholder) {
this._placeholderTemplate = placeholder;
}

_resetPlaceholderTemplate(placeholder: CdkDragPlaceholder) {
if (placeholder === this._placeholderTemplate) {
this._placeholderTemplate = null;
}
}

/** Syncs the root element with the `DragRef`. */
private _updateRootElement() {
const element = this.element.nativeElement as HTMLElement;
Expand Down Expand Up @@ -559,30 +587,25 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
/** Sets up the listener that syncs the handles with the drag ref. */
private _setupHandlesListener() {
// Listen for any newly-added handles.
this._handles.changes
this._handles
.pipe(
startWith(this._handles),
// Sync the new handles with the DragRef.
tap((handles: QueryList<CdkDragHandle>) => {
const childHandleElements = handles
.filter(handle => handle._parentDrag === this)
.map(handle => handle.element);
tap(handles => {
const handleElements = handles.map(handle => handle.element);

// Usually handles are only allowed to be a descendant of the drag element, but if
// the consumer defined a different drag root, we should allow the drag element
// itself to be a handle too.
if (this._selfHandle && this.rootElementSelector) {
childHandleElements.push(this.element);
handleElements.push(this.element);
}

this._dragRef.withHandles(childHandleElements);
this._dragRef.withHandles(handleElements);
}),
// Listen if the state of any of the handles changes.
switchMap((handles: QueryList<CdkDragHandle>) => {
switchMap((handles: CdkDragHandle[]) => {
return merge(
...handles.map(item => {
return item._stateChanges.pipe(startWith(item));
}),
...handles.map(item => item._stateChanges.pipe(startWith(item))),
) as Observable<CdkDragHandle>;
}),
takeUntil(this._destroyed),
Expand Down
3 changes: 2 additions & 1 deletion src/cdk/drag-drop/drag-parent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
*/

import {InjectionToken} from '@angular/core';
import type {CdkDrag} from './directives/drag';

/**
* Injection token that can be used for a `CdkDrag` to provide itself as a parent to the
* drag-specific child directive (`CdkDragHandle`, `CdkDragPreview` etc.). Used primarily
* to avoid circular imports.
* @docs-private
*/
export const CDK_DRAG_PARENT = new InjectionToken<{}>('CDK_DRAG_PARENT');
export const CDK_DRAG_PARENT = new InjectionToken<CdkDrag>('CDK_DRAG_PARENT');
31 changes: 21 additions & 10 deletions tools/public_api_guard/cdk/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { NumberInput } from '@angular/cdk/coercion';
import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { QueryList } from '@angular/core';
import { ScrollDispatcher } from '@angular/cdk/scrolling';
import { SimpleChanges } from '@angular/core';
import { Subject } from 'rxjs';
Expand All @@ -33,7 +32,7 @@ export const CDK_DRAG_CONFIG: InjectionToken<DragDropConfig>;
export const CDK_DRAG_HANDLE: InjectionToken<CdkDragHandle>;

// @public
export const CDK_DRAG_PARENT: InjectionToken<{}>;
export const CDK_DRAG_PARENT: InjectionToken<CdkDrag<any>>;

// @public
export const CDK_DRAG_PLACEHOLDER: InjectionToken<CdkDragPlaceholder<any>>;
Expand All @@ -53,6 +52,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
element: ElementRef<HTMLElement>,
dropContainer: CdkDropList,
_document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, _parentDrag?: CdkDrag<any> | undefined);
// (undocumented)
_addHandle(handle: CdkDragHandle): void;
boundaryElement: string | ElementRef<HTMLElement> | HTMLElement;
constrainPosition?: (userPointerPosition: Point, dragRef: DragRef, dimensions: DOMRect, pickupPositionInElement: Point) => Point;
data: T;
Expand All @@ -70,7 +71,6 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
getFreeDragPosition(): Readonly<Point>;
getPlaceholderElement(): HTMLElement;
getRootElement(): HTMLElement;
_handles: QueryList<CdkDragHandle>;
lockAxis: DragAxis;
readonly moved: Observable<CdkDragMove<T>>;
// (undocumented)
Expand All @@ -81,17 +81,25 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void;
// (undocumented)
ngOnDestroy(): void;
_placeholderTemplate: CdkDragPlaceholder;
previewClass: string | string[];
previewContainer: PreviewContainer;
_previewTemplate: CdkDragPreview;
readonly released: EventEmitter<CdkDragRelease>;
// (undocumented)
_removeHandle(handle: CdkDragHandle): void;
reset(): void;
// (undocumented)
_resetPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
// (undocumented)
_resetPreviewTemplate(preview: CdkDragPreview): void;
rootElementSelector: string;
setFreeDragPosition(value: Point): void;
// (undocumented)
_setPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
// (undocumented)
_setPreviewTemplate(preview: CdkDragPreview): void;
readonly started: EventEmitter<CdkDragStart>;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, ["_previewTemplate", "_placeholderTemplate", "_handles"], never, true, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CdkDrag<any>, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>;
}
Expand Down Expand Up @@ -144,7 +152,7 @@ export interface CdkDragExit<T = any, I = T> {

// @public
export class CdkDragHandle implements OnDestroy {
constructor(element: ElementRef<HTMLElement>, parentDrag?: any);
constructor(element: ElementRef<HTMLElement>, _parentDrag?: CdkDrag<any> | undefined);
get disabled(): boolean;
set disabled(value: boolean);
// (undocumented)
Expand All @@ -153,7 +161,6 @@ export class CdkDragHandle implements OnDestroy {
static ngAcceptInputType_disabled: unknown;
// (undocumented)
ngOnDestroy(): void;
_parentDrag: {} | undefined;
readonly _stateChanges: Subject<CdkDragHandle>;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDragHandle, "[cdkDragHandle]", never, { "disabled": { "alias": "cdkDragHandleDisabled"; "required": false; }; }, {}, never, never, true, never>;
Expand All @@ -180,10 +187,12 @@ export interface CdkDragMove<T = any> {
}

// @public
export class CdkDragPlaceholder<T = any> {
export class CdkDragPlaceholder<T = any> implements OnDestroy {
constructor(templateRef: TemplateRef<T>);
data: T;
// (undocumented)
ngOnDestroy(): void;
// (undocumented)
templateRef: TemplateRef<T>;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDragPlaceholder<any>, "ng-template[cdkDragPlaceholder]", never, { "data": { "alias": "data"; "required": false; }; }, {}, never, never, true, never>;
Expand All @@ -192,13 +201,15 @@ export class CdkDragPlaceholder<T = any> {
}

// @public
export class CdkDragPreview<T = any> {
export class CdkDragPreview<T = any> implements OnDestroy {
constructor(templateRef: TemplateRef<T>);
data: T;
matchSize: boolean;
// (undocumented)
static ngAcceptInputType_matchSize: unknown;
// (undocumented)
ngOnDestroy(): void;
// (undocumented)
templateRef: TemplateRef<T>;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDragPreview<any>, "ng-template[cdkDragPreview]", never, { "data": { "alias": "data"; "required": false; }; "matchSize": { "alias": "matchSize"; "required": false; }; }, {}, never, never, true, never>;
Expand Down

0 comments on commit 4af777a

Please sign in to comment.