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

fix(cdk/drag-drop): resolve helper directives with DI for proper hostDirectives support #28633

Merged
merged 1 commit into from
Feb 26, 2024
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
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
Loading