Skip to content

Commit ef68e32

Browse files
authored
fix(cdk/drag-drop): resolve helper directives with DI for proper hostDirectives 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.
1 parent 8f60b62 commit ef68e32

File tree

6 files changed

+110
-49
lines changed

6 files changed

+110
-49
lines changed

src/cdk/drag-drop/directives/drag-handle.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
booleanAttribute,
1919
} from '@angular/core';
2020
import {Subject} from 'rxjs';
21+
import type {CdkDrag} from './drag';
2122
import {CDK_DRAG_PARENT} from '../drag-parent';
2223
import {assertElementNode} from './assertions';
2324

@@ -38,9 +39,6 @@ export const CDK_DRAG_HANDLE = new InjectionToken<CdkDragHandle>('CdkDragHandle'
3839
providers: [{provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle}],
3940
})
4041
export class CdkDragHandle implements OnDestroy {
41-
/** Closest parent draggable instance. */
42-
_parentDrag: {} | undefined;
43-
4442
/** Emits when the state of the handle has changed. */
4543
readonly _stateChanges = new Subject<CdkDragHandle>();
4644

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

5856
constructor(
5957
public element: ElementRef<HTMLElement>,
60-
@Inject(CDK_DRAG_PARENT) @Optional() @SkipSelf() parentDrag?: any,
58+
@Inject(CDK_DRAG_PARENT) @Optional() @SkipSelf() private _parentDrag?: CdkDrag,
6159
) {
6260
if (typeof ngDevMode === 'undefined' || ngDevMode) {
6361
assertElementNode(element.nativeElement, 'cdkDragHandle');
6462
}
6563

66-
this._parentDrag = parentDrag;
64+
_parentDrag?._addHandle(this);
6765
}
6866

6967
ngOnDestroy() {
68+
this._parentDrag?._removeHandle(this);
7069
this._stateChanges.complete();
7170
}
7271
}

src/cdk/drag-drop/directives/drag-placeholder.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

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

1112
/**
1213
* Injection token that can be used to reference instances of `CdkDragPlaceholder`. It serves as
@@ -24,8 +25,17 @@ export const CDK_DRAG_PLACEHOLDER = new InjectionToken<CdkDragPlaceholder>('CdkD
2425
standalone: true,
2526
providers: [{provide: CDK_DRAG_PLACEHOLDER, useExisting: CdkDragPlaceholder}],
2627
})
27-
export class CdkDragPlaceholder<T = any> {
28+
export class CdkDragPlaceholder<T = any> implements OnDestroy {
29+
private _drag = inject(CDK_DRAG_PARENT);
30+
2831
/** Context data to be added to the placeholder template instance. */
2932
@Input() data: T;
30-
constructor(public templateRef: TemplateRef<T>) {}
33+
34+
constructor(public templateRef: TemplateRef<T>) {
35+
this._drag._setPlaceholderTemplate(this);
36+
}
37+
38+
ngOnDestroy(): void {
39+
this._drag._resetPlaceholderTemplate(this);
40+
}
3141
}

src/cdk/drag-drop/directives/drag-preview.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, InjectionToken, Input, TemplateRef, booleanAttribute} from '@angular/core';
9+
import {
10+
Directive,
11+
InjectionToken,
12+
Input,
13+
OnDestroy,
14+
TemplateRef,
15+
booleanAttribute,
16+
inject,
17+
} from '@angular/core';
18+
import {CDK_DRAG_PARENT} from '../drag-parent';
1019

1120
/**
1221
* Injection token that can be used to reference instances of `CdkDragPreview`. It serves as
@@ -24,12 +33,20 @@ export const CDK_DRAG_PREVIEW = new InjectionToken<CdkDragPreview>('CdkDragPrevi
2433
standalone: true,
2534
providers: [{provide: CDK_DRAG_PREVIEW, useExisting: CdkDragPreview}],
2635
})
27-
export class CdkDragPreview<T = any> {
36+
export class CdkDragPreview<T = any> implements OnDestroy {
37+
private _drag = inject(CDK_DRAG_PARENT);
38+
2839
/** Context data to be added to the preview template instance. */
2940
@Input() data: T;
3041

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

34-
constructor(public templateRef: TemplateRef<T>) {}
45+
constructor(public templateRef: TemplateRef<T>) {
46+
this._drag._setPreviewTemplate(this);
47+
}
48+
49+
ngOnDestroy(): void {
50+
this._drag._resetPreviewTemplate(this);
51+
}
3552
}

src/cdk/drag-drop/directives/drag.ts

+50-27
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {Directionality} from '@angular/cdk/bidi';
1010
import {DOCUMENT} from '@angular/common';
1111
import {
1212
AfterViewInit,
13-
ContentChild,
14-
ContentChildren,
1513
Directive,
1614
ElementRef,
1715
EventEmitter,
@@ -21,7 +19,6 @@ import {
2119
OnDestroy,
2220
Optional,
2321
Output,
24-
QueryList,
2522
SkipSelf,
2623
ViewContainerRef,
2724
OnChanges,
@@ -32,7 +29,7 @@ import {
3229
booleanAttribute,
3330
} from '@angular/core';
3431
import {coerceElement, coerceNumberProperty} from '@angular/cdk/coercion';
35-
import {Observable, Observer, Subject, merge} from 'rxjs';
32+
import {BehaviorSubject, Observable, Observer, Subject, merge} from 'rxjs';
3633
import {startWith, take, map, takeUntil, switchMap, tap} from 'rxjs/operators';
3734
import type {
3835
CdkDragDrop,
@@ -44,8 +41,8 @@ import type {
4441
CdkDragRelease,
4542
} from '../drag-events';
4643
import {CDK_DRAG_HANDLE, CdkDragHandle} from './drag-handle';
47-
import {CDK_DRAG_PLACEHOLDER, CdkDragPlaceholder} from './drag-placeholder';
48-
import {CDK_DRAG_PREVIEW, CdkDragPreview} from './drag-preview';
44+
import {CdkDragPlaceholder} from './drag-placeholder';
45+
import {CdkDragPreview} from './drag-preview';
4946
import {CDK_DRAG_PARENT} from '../drag-parent';
5047
import {DragRef, Point, PreviewContainer} from '../drag-ref';
5148
import type {CdkDropList} from './drop-list';
@@ -77,19 +74,13 @@ export const CDK_DROP_LIST = new InjectionToken<CdkDropList>('CdkDropList');
7774
export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
7875
private readonly _destroyed = new Subject<void>();
7976
private static _dragInstances: CdkDrag[] = [];
77+
private _handles = new BehaviorSubject<CdkDragHandle[]>([]);
78+
private _previewTemplate: CdkDragPreview | null;
79+
private _placeholderTemplate: CdkDragPlaceholder | null;
8080

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

84-
/** Elements that can be used to drag the draggable item. */
85-
@ContentChildren(CDK_DRAG_HANDLE, {descendants: true}) _handles: QueryList<CdkDragHandle>;
86-
87-
/** Element that will be used as a template to create the draggable item's preview. */
88-
@ContentChild(CDK_DRAG_PREVIEW) _previewTemplate: CdkDragPreview;
89-
90-
/** Template for placeholder element rendered to show where a draggable would be dropped. */
91-
@ContentChild(CDK_DRAG_PLACEHOLDER) _placeholderTemplate: CdkDragPlaceholder;
92-
9384
/** Arbitrary data to attach to this drag instance. */
9485
@Input('cdkDragData') data: T;
9586

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

352343
// Unnecessary in most cases, but used to avoid extra change detections with `zone-paths-rxjs`.
353344
this._ngZone.runOutsideAngular(() => {
345+
this._handles.complete();
354346
this._destroyed.next();
355347
this._destroyed.complete();
356348
this._dragRef.dispose();
357349
});
358350
}
359351

352+
_addHandle(handle: CdkDragHandle) {
353+
const handles = this._handles.getValue();
354+
handles.push(handle);
355+
this._handles.next(handles);
356+
}
357+
358+
_removeHandle(handle: CdkDragHandle) {
359+
const handles = this._handles.getValue();
360+
const index = handles.indexOf(handle);
361+
362+
if (index > -1) {
363+
handles.splice(index, 1);
364+
this._handles.next(handles);
365+
}
366+
}
367+
368+
_setPreviewTemplate(preview: CdkDragPreview) {
369+
this._previewTemplate = preview;
370+
}
371+
372+
_resetPreviewTemplate(preview: CdkDragPreview) {
373+
if (preview === this._previewTemplate) {
374+
this._previewTemplate = null;
375+
}
376+
}
377+
378+
_setPlaceholderTemplate(placeholder: CdkDragPlaceholder) {
379+
this._placeholderTemplate = placeholder;
380+
}
381+
382+
_resetPlaceholderTemplate(placeholder: CdkDragPlaceholder) {
383+
if (placeholder === this._placeholderTemplate) {
384+
this._placeholderTemplate = null;
385+
}
386+
}
387+
360388
/** Syncs the root element with the `DragRef`. */
361389
private _updateRootElement() {
362390
const element = this.element.nativeElement as HTMLElement;
@@ -559,30 +587,25 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
559587
/** Sets up the listener that syncs the handles with the drag ref. */
560588
private _setupHandlesListener() {
561589
// Listen for any newly-added handles.
562-
this._handles.changes
590+
this._handles
563591
.pipe(
564-
startWith(this._handles),
565592
// Sync the new handles with the DragRef.
566-
tap((handles: QueryList<CdkDragHandle>) => {
567-
const childHandleElements = handles
568-
.filter(handle => handle._parentDrag === this)
569-
.map(handle => handle.element);
593+
tap(handles => {
594+
const handleElements = handles.map(handle => handle.element);
570595

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

578-
this._dragRef.withHandles(childHandleElements);
603+
this._dragRef.withHandles(handleElements);
579604
}),
580605
// Listen if the state of any of the handles changes.
581-
switchMap((handles: QueryList<CdkDragHandle>) => {
606+
switchMap((handles: CdkDragHandle[]) => {
582607
return merge(
583-
...handles.map(item => {
584-
return item._stateChanges.pipe(startWith(item));
585-
}),
608+
...handles.map(item => item._stateChanges.pipe(startWith(item))),
586609
) as Observable<CdkDragHandle>;
587610
}),
588611
takeUntil(this._destroyed),

src/cdk/drag-drop/drag-parent.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
*/
88

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

1112
/**
1213
* Injection token that can be used for a `CdkDrag` to provide itself as a parent to the
1314
* drag-specific child directive (`CdkDragHandle`, `CdkDragPreview` etc.). Used primarily
1415
* to avoid circular imports.
1516
* @docs-private
1617
*/
17-
export const CDK_DRAG_PARENT = new InjectionToken<{}>('CDK_DRAG_PARENT');
18+
export const CDK_DRAG_PARENT = new InjectionToken<CdkDrag>('CDK_DRAG_PARENT');

tools/public_api_guard/cdk/drag-drop.md

+21-10
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { NumberInput } from '@angular/cdk/coercion';
1818
import { Observable } from 'rxjs';
1919
import { OnChanges } from '@angular/core';
2020
import { OnDestroy } from '@angular/core';
21-
import { QueryList } from '@angular/core';
2221
import { ScrollDispatcher } from '@angular/cdk/scrolling';
2322
import { SimpleChanges } from '@angular/core';
2423
import { Subject } from 'rxjs';
@@ -33,7 +32,7 @@ export const CDK_DRAG_CONFIG: InjectionToken<DragDropConfig>;
3332
export const CDK_DRAG_HANDLE: InjectionToken<CdkDragHandle>;
3433

3534
// @public
36-
export const CDK_DRAG_PARENT: InjectionToken<{}>;
35+
export const CDK_DRAG_PARENT: InjectionToken<CdkDrag<any>>;
3736

3837
// @public
3938
export const CDK_DRAG_PLACEHOLDER: InjectionToken<CdkDragPlaceholder<any>>;
@@ -53,6 +52,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
5352
element: ElementRef<HTMLElement>,
5453
dropContainer: CdkDropList,
5554
_document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, _parentDrag?: CdkDrag<any> | undefined);
55+
// (undocumented)
56+
_addHandle(handle: CdkDragHandle): void;
5657
boundaryElement: string | ElementRef<HTMLElement> | HTMLElement;
5758
constrainPosition?: (userPointerPosition: Point, dragRef: DragRef, dimensions: DOMRect, pickupPositionInElement: Point) => Point;
5859
data: T;
@@ -70,7 +71,6 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
7071
getFreeDragPosition(): Readonly<Point>;
7172
getPlaceholderElement(): HTMLElement;
7273
getRootElement(): HTMLElement;
73-
_handles: QueryList<CdkDragHandle>;
7474
lockAxis: DragAxis;
7575
readonly moved: Observable<CdkDragMove<T>>;
7676
// (undocumented)
@@ -81,17 +81,25 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
8181
ngOnChanges(changes: SimpleChanges): void;
8282
// (undocumented)
8383
ngOnDestroy(): void;
84-
_placeholderTemplate: CdkDragPlaceholder;
8584
previewClass: string | string[];
8685
previewContainer: PreviewContainer;
87-
_previewTemplate: CdkDragPreview;
8886
readonly released: EventEmitter<CdkDragRelease>;
87+
// (undocumented)
88+
_removeHandle(handle: CdkDragHandle): void;
8989
reset(): void;
90+
// (undocumented)
91+
_resetPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
92+
// (undocumented)
93+
_resetPreviewTemplate(preview: CdkDragPreview): void;
9094
rootElementSelector: string;
9195
setFreeDragPosition(value: Point): void;
96+
// (undocumented)
97+
_setPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
98+
// (undocumented)
99+
_setPreviewTemplate(preview: CdkDragPreview): void;
92100
readonly started: EventEmitter<CdkDragStart>;
93101
// (undocumented)
94-
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>;
102+
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>;
95103
// (undocumented)
96104
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; }]>;
97105
}
@@ -144,7 +152,7 @@ export interface CdkDragExit<T = any, I = T> {
144152

145153
// @public
146154
export class CdkDragHandle implements OnDestroy {
147-
constructor(element: ElementRef<HTMLElement>, parentDrag?: any);
155+
constructor(element: ElementRef<HTMLElement>, _parentDrag?: CdkDrag<any> | undefined);
148156
get disabled(): boolean;
149157
set disabled(value: boolean);
150158
// (undocumented)
@@ -153,7 +161,6 @@ export class CdkDragHandle implements OnDestroy {
153161
static ngAcceptInputType_disabled: unknown;
154162
// (undocumented)
155163
ngOnDestroy(): void;
156-
_parentDrag: {} | undefined;
157164
readonly _stateChanges: Subject<CdkDragHandle>;
158165
// (undocumented)
159166
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDragHandle, "[cdkDragHandle]", never, { "disabled": { "alias": "cdkDragHandleDisabled"; "required": false; }; }, {}, never, never, true, never>;
@@ -180,10 +187,12 @@ export interface CdkDragMove<T = any> {
180187
}
181188

182189
// @public
183-
export class CdkDragPlaceholder<T = any> {
190+
export class CdkDragPlaceholder<T = any> implements OnDestroy {
184191
constructor(templateRef: TemplateRef<T>);
185192
data: T;
186193
// (undocumented)
194+
ngOnDestroy(): void;
195+
// (undocumented)
187196
templateRef: TemplateRef<T>;
188197
// (undocumented)
189198
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDragPlaceholder<any>, "ng-template[cdkDragPlaceholder]", never, { "data": { "alias": "data"; "required": false; }; }, {}, never, never, true, never>;
@@ -192,13 +201,15 @@ export class CdkDragPlaceholder<T = any> {
192201
}
193202

194203
// @public
195-
export class CdkDragPreview<T = any> {
204+
export class CdkDragPreview<T = any> implements OnDestroy {
196205
constructor(templateRef: TemplateRef<T>);
197206
data: T;
198207
matchSize: boolean;
199208
// (undocumented)
200209
static ngAcceptInputType_matchSize: unknown;
201210
// (undocumented)
211+
ngOnDestroy(): void;
212+
// (undocumented)
202213
templateRef: TemplateRef<T>;
203214
// (undocumented)
204215
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDragPreview<any>, "ng-template[cdkDragPreview]", never, { "data": { "alias": "data"; "required": false; }; "matchSize": { "alias": "matchSize"; "required": false; }; }, {}, never, never, true, never>;

0 commit comments

Comments
 (0)