Skip to content

Commit 5824450

Browse files
crisbetoVivian Hu
authored andcommitted
refactor(drag-drop): clean up DragRef public API (#14654)
Cleans up the API of the DragRef before we can expose it through the public API by making the handle, custom placeholder and custom preview easier to configure.
1 parent c0050cd commit 5824450

File tree

5 files changed

+120
-76
lines changed

5 files changed

+120
-76
lines changed

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, ElementRef, Inject, Optional, Input} from '@angular/core';
9+
import {Directive, ElementRef, Inject, Optional, Input, OnDestroy} from '@angular/core';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11+
import {Subject} from 'rxjs';
1112
import {CDK_DRAG_PARENT} from '../drag-parent';
1213
import {toggleNativeDragInteractions} from '../drag-styling';
1314

@@ -18,15 +19,19 @@ import {toggleNativeDragInteractions} from '../drag-styling';
1819
'class': 'cdk-drag-handle'
1920
}
2021
})
21-
export class CdkDragHandle {
22+
export class CdkDragHandle implements OnDestroy {
2223
/** Closest parent draggable instance. */
2324
_parentDrag: {} | undefined;
2425

26+
/** Emits when the state of the handle has changed. */
27+
_stateChanges = new Subject<CdkDragHandle>();
28+
2529
/** Whether starting to drag through this handle is disabled. */
2630
@Input('cdkDragHandleDisabled')
2731
get disabled(): boolean { return this._disabled; }
2832
set disabled(value: boolean) {
2933
this._disabled = coerceBooleanProperty(value);
34+
this._stateChanges.next(this);
3035
}
3136
private _disabled = false;
3237

@@ -37,4 +42,8 @@ export class CdkDragHandle {
3742
this._parentDrag = parentDrag;
3843
toggleNativeDragInteractions(element.nativeElement, false);
3944
}
45+
46+
ngOnDestroy() {
47+
this._stateChanges.complete();
48+
}
4049
}

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

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import {
3030
SimpleChanges,
3131
} from '@angular/core';
3232
import {coerceBooleanProperty} from '@angular/cdk/coercion';
33-
import {Observable, Subscription, Observer} from 'rxjs';
34-
import {startWith, take, map} from 'rxjs/operators';
33+
import {Observable, Observer, Subject, merge} from 'rxjs';
34+
import {startWith, take, map, takeUntil, switchMap, tap} from 'rxjs/operators';
3535
import {DragDropRegistry} from '../drag-drop-registry';
3636
import {
3737
CdkDragDrop,
@@ -73,8 +73,7 @@ export function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig {
7373
providers: [{provide: CDK_DRAG_PARENT, useExisting: CdkDrag}]
7474
})
7575
export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
76-
/** Subscription to the stream that initializes the root element. */
77-
private _rootElementInitSubscription = Subscription.EMPTY;
76+
private _destroyed = new Subject<void>();
7877

7978
/** Reference to the underlying drag instance. */
8079
_dragRef: DragRef<CdkDrag<T>>;
@@ -178,16 +177,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
178177
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
179178
this._dir);
180179
ref.data = this;
181-
ref.beforeStarted.subscribe(() => {
182-
if (!ref.isDragging()) {
183-
ref.disabled = this.disabled;
184-
ref.lockAxis = this.lockAxis;
185-
ref
186-
.withBoundaryElement(this._getBoundaryElement())
187-
.withPlaceholderTemplate(this._placeholderTemplate)
188-
.withPreviewTemplate(this._previewTemplate);
189-
}
190-
});
180+
this._syncInputs(ref);
191181
this._proxyEvents(ref);
192182
}
193183

@@ -214,15 +204,32 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
214204
// element to be in the proper place in the DOM. This is mostly relevant
215205
// for draggable elements inside portals since they get stamped out in
216206
// their original DOM position and then they get transferred to the portal.
217-
this._rootElementInitSubscription = this._ngZone.onStable.asObservable()
218-
.pipe(take(1))
207+
this._ngZone.onStable.asObservable()
208+
.pipe(take(1), takeUntil(this._destroyed))
219209
.subscribe(() => {
220210
this._updateRootElement();
221-
this._handles.changes
222-
.pipe(startWith(this._handles))
223-
.subscribe((handleList: QueryList<CdkDragHandle>) => {
224-
this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this));
225-
});
211+
212+
// Listen for any newly-added handles.
213+
this._handles.changes.pipe(
214+
startWith(this._handles),
215+
// Sync the new handles with the DragRef.
216+
tap((handles: QueryList<CdkDragHandle>) => {
217+
const childHandleElements = handles
218+
.filter(handle => handle._parentDrag === this)
219+
.map(handle => handle.element);
220+
this._dragRef.withHandles(childHandleElements);
221+
}),
222+
// Listen if the state of any of the handles changes.
223+
switchMap((handles: QueryList<CdkDragHandle>) => {
224+
return merge(...handles.map(item => item._stateChanges));
225+
}),
226+
takeUntil(this._destroyed)
227+
).subscribe(handleInstance => {
228+
// Enabled/disable the handle that changed in the DragRef.
229+
const dragRef = this._dragRef;
230+
const handle = handleInstance.element.nativeElement;
231+
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
232+
});
226233
});
227234
}
228235

@@ -237,7 +244,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
237244
}
238245

239246
ngOnDestroy() {
240-
this._rootElementInitSubscription.unsubscribe();
247+
this._destroyed.next();
248+
this._destroyed.complete();
241249
this._dragRef.dispose();
242250
}
243251

@@ -261,6 +269,23 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
261269
return selector ? getClosestMatchingAncestor(this.element.nativeElement, selector) : null;
262270
}
263271

272+
/** Syncs the inputs of the CdkDrag with the options of the underlying DragRef. */
273+
private _syncInputs(ref: DragRef<CdkDrag<T>>) {
274+
ref.beforeStarted.subscribe(() => {
275+
if (!ref.isDragging()) {
276+
const {_placeholderTemplate: placeholder, _previewTemplate: preview} = this;
277+
278+
ref.disabled = this.disabled;
279+
ref.lockAxis = this.lockAxis;
280+
ref.withBoundaryElement(this._getBoundaryElement());
281+
placeholder ? ref.withPlaceholderTemplate(placeholder.templateRef, placeholder.data) :
282+
ref.withPlaceholderTemplate(null);
283+
preview ? ref.withPreviewTemplate(preview.templateRef, preview.data) :
284+
ref.withPreviewTemplate(null);
285+
}
286+
});
287+
}
288+
264289
/**
265290
* Proxies the events from a DragRef to events that
266291
* match the interfaces of the CdkDrag outputs.

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

Lines changed: 58 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from
1010
import {ViewportRuler} from '@angular/cdk/scrolling';
1111
import {Directionality} from '@angular/cdk/bidi';
1212
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
13-
import {coerceBooleanProperty} from '@angular/cdk/coercion';
13+
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
1414
import {Subscription, Subject, Observable, Observer} from 'rxjs';
1515
import {DropListRefInternal as DropListRef} from './drop-list-ref';
1616
import {DragDropRegistry} from './drag-drop-registry';
@@ -46,24 +46,10 @@ const activeEventListenerOptions = normalizePassiveListenerOptions({passive: fal
4646
*/
4747
const MOUSE_EVENT_IGNORE_TIME = 800;
4848

49-
/**
50-
* Template that can be used to create a drag helper element (e.g. a preview or a placeholder).
51-
*/
52-
interface DragHelperTemplate<T = any> {
53-
templateRef: TemplateRef<T>;
54-
data: T;
55-
}
56-
57-
interface DragHandle {
58-
element: ElementRef<HTMLElement>;
59-
disabled: boolean;
60-
}
61-
6249
// TODO(crisbeto): add auto-scrolling functionality.
6350
// TODO(crisbeto): add an API for moving a draggable up/down the
6451
// list programmatically. Useful for keyboard controls.
6552

66-
6753
/**
6854
* Internal compile-time-only representation of a `DragRef`.
6955
* Used to avoid circular import issues between the `DragRef` and the `DropListRef`.
@@ -187,13 +173,16 @@ export class DragRef<T = any> {
187173
private _boundaryRect?: ClientRect;
188174

189175
/** Element that will be used as a template to create the draggable item's preview. */
190-
private _previewTemplate: DragHelperTemplate | null;
176+
private _previewTemplate?: {template: TemplateRef<any> | null, context?: any};
191177

192178
/** Template for placeholder element rendered to show where a draggable would be dropped. */
193-
private _placeholderTemplate: DragHelperTemplate | null;
179+
private _placeholderTemplate?: {template: TemplateRef<any> | null, context?: any};
194180

195181
/** Elements that can be used to drag the draggable item. */
196-
private _handles: DragHandle[] = [];
182+
private _handles: HTMLElement[] = [];
183+
184+
/** Registered handles that are currently disabled. */
185+
private _disabledHandles = new Set<HTMLElement>();
197186

198187
/** Axis along which dragging is locked. */
199188
lockAxis: 'x' | 'y';
@@ -292,36 +281,40 @@ export class DragRef<T = any> {
292281
}
293282

294283
/** Registers the handles that can be used to drag the element. */
295-
withHandles(handles: DragHandle[]): this {
296-
// TODO(crisbeto): have this accept HTMLElement[] | ElementRef<HTMLElement>[]
297-
this._handles = handles;
298-
handles.forEach(handle => toggleNativeDragInteractions(handle.element.nativeElement, false));
284+
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this {
285+
this._handles = handles.map(handle => coerceElement(handle));
286+
this._handles.forEach(handle => toggleNativeDragInteractions(handle, false));
299287
this._toggleNativeDragInteractions();
300288
return this;
301289
}
302290

303-
/** Registers the template that should be used for the drag preview. */
304-
withPreviewTemplate(template: DragHelperTemplate | null): this {
305-
// TODO(crisbeto): have this accept a TemplateRef
306-
this._previewTemplate = template;
291+
/**
292+
* Registers the template that should be used for the drag preview.
293+
* @param template Template that from which to stamp out the preview.
294+
* @param context Variables to add to the template's context.
295+
*/
296+
withPreviewTemplate(template: TemplateRef<any> | null, context?: any): this {
297+
this._previewTemplate = {template, context};
307298
return this;
308299
}
309300

310-
/** Registers the template that should be used for the drag placeholder. */
311-
withPlaceholderTemplate(template: DragHelperTemplate | null): this {
312-
// TODO(crisbeto): have this accept a TemplateRef
313-
this._placeholderTemplate = template;
301+
/**
302+
* Registers the template that should be used for the drag placeholder.
303+
* @param template Template that from which to stamp out the placeholder.
304+
* @param context Variables to add to the template's context.
305+
*/
306+
withPlaceholderTemplate(template: TemplateRef<any> | null, context?: any): this {
307+
this._placeholderTemplate = {template, context};
314308
return this;
315309
}
316310

317-
318311
/**
319312
* Sets an alternate drag root element. The root element is the element that will be moved as
320313
* the user is dragging. Passing an alternate root element is useful when trying to enable
321314
* dragging on an element that you might not have access to.
322315
*/
323316
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this {
324-
const element = rootElement instanceof ElementRef ? rootElement.nativeElement : rootElement;
317+
const element = coerceElement(rootElement);
325318

326319
if (element !== this._rootElement) {
327320
if (this._rootElement) {
@@ -340,8 +333,7 @@ export class DragRef<T = any> {
340333
* Element to which the draggable's position will be constrained.
341334
*/
342335
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this {
343-
this._boundaryElement = boundaryElement instanceof ElementRef ?
344-
boundaryElement.nativeElement : boundaryElement;
336+
this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null;
345337
return this;
346338
}
347339

@@ -370,6 +362,7 @@ export class DragRef<T = any> {
370362
this.dropped.complete();
371363
this._moveEvents.complete();
372364
this._handles = [];
365+
this._disabledHandles.clear();
373366
this._boundaryElement = this._rootElement = this._placeholderTemplate =
374367
this._previewTemplate = this._nextSibling = null!;
375368
}
@@ -386,6 +379,24 @@ export class DragRef<T = any> {
386379
this._passiveTransform = {x: 0, y: 0};
387380
}
388381

382+
/**
383+
* Sets a handle as disabled. While a handle is disabled, it'll capture and interrupt dragging.
384+
* @param handle Handle element that should be disabled.
385+
*/
386+
disableHandle(handle: HTMLElement) {
387+
if (this._handles.indexOf(handle) > -1) {
388+
this._disabledHandles.add(handle);
389+
}
390+
}
391+
392+
/**
393+
* Enables a handle, if it has been disabled.
394+
* @param handle Handle element to be enabled.
395+
*/
396+
enableHandle(handle: HTMLElement) {
397+
this._disabledHandles.delete(handle);
398+
}
399+
389400
/** Unsubscribes from the global subscriptions. */
390401
private _removeSubscriptions() {
391402
this._pointerMoveSubscription.unsubscribe();
@@ -418,21 +429,19 @@ export class DragRef<T = any> {
418429
this._placeholder = this._placeholderRef = null!;
419430
}
420431

421-
422432
/** Handler for the `mousedown`/`touchstart` events. */
423433
private _pointerDown = (event: MouseEvent | TouchEvent) => {
424434
this.beforeStarted.next();
425435

426436
// Delegate the event based on whether it started from a handle or the element itself.
427437
if (this._handles.length) {
428438
const targetHandle = this._handles.find(handle => {
429-
const element = handle.element.nativeElement;
430439
const target = event.target;
431-
return !!target && (target === element || element.contains(target as HTMLElement));
440+
return !!target && (target === handle || handle.contains(target as HTMLElement));
432441
});
433442

434-
if (targetHandle && !targetHandle.disabled && !this.disabled) {
435-
this._initializeDragSequence(targetHandle.element.nativeElement, event);
443+
if (targetHandle && !this._disabledHandles.has(targetHandle) && !this.disabled) {
444+
this._initializeDragSequence(targetHandle, event);
436445
}
437446
} else if (!this.disabled) {
438447
this._initializeDragSequence(this._rootElement, event);
@@ -638,7 +647,8 @@ export class DragRef<T = any> {
638647

639648
// If we have a custom preview template, the element won't be visible anyway so we avoid the
640649
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
641-
this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} :
650+
this._pickupPositionInElement = this._previewTemplate && this._previewTemplate.template ?
651+
{x: 0, y: 0} :
642652
this._getPointerPositionInElement(referenceElement, event);
643653
const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
644654
this._pointerDirectionDelta = {x: 0, y: 0};
@@ -724,12 +734,12 @@ export class DragRef<T = any> {
724734
* and will be used as a preview of the element that is being dragged.
725735
*/
726736
private _createPreviewElement(): HTMLElement {
737+
const previewTemplate = this._previewTemplate;
727738
let preview: HTMLElement;
728739

729-
if (this._previewTemplate) {
730-
const viewRef = this._viewContainerRef.createEmbeddedView(this._previewTemplate.templateRef,
731-
this._previewTemplate.data);
732-
740+
if (previewTemplate && previewTemplate.template) {
741+
const viewRef = this._viewContainerRef.createEmbeddedView(previewTemplate.template,
742+
previewTemplate.context);
733743
preview = viewRef.rootNodes[0];
734744
this._previewRef = viewRef;
735745
preview.style.transform =
@@ -811,12 +821,13 @@ export class DragRef<T = any> {
811821

812822
/** Creates an element that will be shown instead of the current element while dragging. */
813823
private _createPlaceholderElement(): HTMLElement {
824+
const placeholderTemplate = this._placeholderTemplate;
814825
let placeholder: HTMLElement;
815826

816-
if (this._placeholderTemplate) {
827+
if (placeholderTemplate && placeholderTemplate.template) {
817828
this._placeholderRef = this._viewContainerRef.createEmbeddedView(
818-
this._placeholderTemplate.templateRef,
819-
this._placeholderTemplate.data
829+
placeholderTemplate.template,
830+
placeholderTemplate.context
820831
);
821832
placeholder = this._placeholderRef.rootNodes[0];
822833
} else {

src/cdk/drag-drop/drop-list-ref.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,7 @@ export class DropListRef<T = any> {
147147
*/
148148
private _previousSwap = {drag: null as DragRef | null, delta: 0};
149149

150-
/**
151-
* Draggable items in the container.
152-
* TODO(crisbeto): support arrays.
153-
*/
150+
/** Draggable items in the container. */
154151
private _draggables: DragRef[];
155152

156153
private _siblings: DropListRef[] = [];

0 commit comments

Comments
 (0)