Skip to content

Commit 011ba9a

Browse files
committed
refactor(drag-drop): clean up DragRef public API
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 d22f48c commit 011ba9a

File tree

5 files changed

+125
-76
lines changed

5 files changed

+125
-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: 52 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,37 @@ 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 ownHandles = handles.reduce((accumulator, handle) => {
218+
if (handle._parentDrag === this) {
219+
accumulator.push(handle.element);
220+
}
221+
222+
return accumulator;
223+
}, [] as ElementRef<HTMLElement>[]);
224+
225+
this._dragRef.withHandles(ownHandles);
226+
}),
227+
// Listen if the state of any of the handles changes.
228+
switchMap((handles: QueryList<CdkDragHandle>) => {
229+
return merge(...handles.map(item => item._stateChanges));
230+
}),
231+
takeUntil(this._destroyed)
232+
).subscribe(handleInstance => {
233+
// Enabled/disable the handle that changed in the DragRef.
234+
const dragRef = this._dragRef;
235+
const handle = handleInstance.element.nativeElement;
236+
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
237+
});
226238
});
227239
}
228240

@@ -237,7 +249,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
237249
}
238250

239251
ngOnDestroy() {
240-
this._rootElementInitSubscription.unsubscribe();
252+
this._destroyed.next();
253+
this._destroyed.complete();
241254
this._dragRef.dispose();
242255
}
243256

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

277+
/** Syncs the inputs of the CdkDrag with the options of the underlying DragRef. */
278+
private _syncInputs(ref: DragRef<CdkDrag<T>>) {
279+
ref.beforeStarted.subscribe(() => {
280+
if (!ref.isDragging()) {
281+
const {_placeholderTemplate: placeholder, _previewTemplate: preview} = this;
282+
283+
ref.disabled = this.disabled;
284+
ref.lockAxis = this.lockAxis;
285+
ref.withBoundaryElement(this._getBoundaryElement());
286+
placeholder ? ref.withPlaceholderTemplate(placeholder.templateRef, placeholder.data) :
287+
ref.withPlaceholderTemplate(null);
288+
preview ? ref.withPreviewTemplate(preview.templateRef, preview.data) :
289+
ref.withPreviewTemplate(null);
290+
}
291+
});
292+
}
293+
264294
/**
265295
* Proxies the events from a DragRef to events that
266296
* 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)