Skip to content

Commit 6a3eb5f

Browse files
committed
feat(drag-drop): add service for attaching drag&drop to arbitrary DOM nodes
* Adds the `DragDrop` service that simplifies the construction logic for `DragRef` and `DropListRef` and allows consumers to attach drag&drop functionality to arbitrary DOM nodes, rather than having to go through the directives. * Reworks `DragRef` and `DragDropRef` to make them easier to construct. * Normalizes some of the instances where some parameters only accept an `ElementRef`, whereas others accept `ElementRef | HTMLElement`.
1 parent e661317 commit 6a3eb5f

File tree

11 files changed

+416
-99
lines changed

11 files changed

+416
-99
lines changed

src/cdk/drag-drop/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ng_test_library(
2323
name = "drag-drop_test_sources",
2424
srcs = glob(["**/*.spec.ts"]),
2525
deps = [
26+
"@rxjs",
2627
"//src/cdk/testing",
2728
"//src/cdk/bidi",
2829
":drag-drop",

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
createTouchEvent,
2222
} from '@angular/cdk/testing';
2323
import {Directionality} from '@angular/cdk/bidi';
24+
import {of as observableOf} from 'rxjs';
2425
import {CdkDrag, CDK_DRAG_CONFIG} from './drag';
2526
import {CdkDragDrop} from '../drag-events';
2627
import {moveItemInArray} from '../drag-utils';
@@ -1140,7 +1141,7 @@ describe('CdkDrag', () => {
11401141
it('should dispatch the correct `dropped` event in RTL horizontal drop zone', fakeAsync(() => {
11411142
const fixture = createComponent(DraggableInHorizontalDropZone, [{
11421143
provide: Directionality,
1143-
useValue: ({value: 'rtl'})
1144+
useValue: ({value: 'rtl', change: observableOf()})
11441145
}]);
11451146

11461147
fixture.nativeElement.setAttribute('dir', 'rtl');
@@ -1296,7 +1297,7 @@ describe('CdkDrag', () => {
12961297
it('should pass the proper direction to the preview in rtl', fakeAsync(() => {
12971298
const fixture = createComponent(DraggableInDropZone, [{
12981299
provide: Directionality,
1299-
useValue: ({value: 'rtl'})
1300+
useValue: ({value: 'rtl', change: observableOf()})
13001301
}]);
13011302

13021303
fixture.detectChanges();

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

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {CDK_DRAG_PARENT} from '../drag-parent';
5050
import {DragRef, DragRefConfig} from '../drag-ref';
5151
import {DropListRef} from '../drop-list-ref';
5252
import {CdkDropListInternal as CdkDropList} from './drop-list';
53+
import {DragDrop} from '../drag-drop';
5354

5455
/** Injection token that can be used to configure the behavior of `CdkDrag`. */
5556
export const CDK_DRAG_CONFIG = new InjectionToken<DragRefConfig>('CDK_DRAG_CONFIG', {
@@ -167,18 +168,30 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
167168
@Inject(DOCUMENT) private _document: any,
168169
private _ngZone: NgZone,
169170
private _viewContainerRef: ViewContainerRef,
170-
private _viewportRuler: ViewportRuler,
171-
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
172-
@Inject(CDK_DRAG_CONFIG) private _config: DragRefConfig,
173-
@Optional() private _dir: Directionality) {
174-
175-
const ref = this._dragRef = new DragRef(element, this._document, this._ngZone,
176-
this._viewContainerRef, this._viewportRuler, this._dragDropRegistry,
177-
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
178-
this._dir);
179-
ref.data = this;
180-
this._syncInputs(ref);
181-
this._proxyEvents(ref);
171+
viewportRuler: ViewportRuler,
172+
dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
173+
@Inject(CDK_DRAG_CONFIG) config: DragRefConfig,
174+
@Optional() private _dir: Directionality,
175+
176+
/**
177+
* @deprecated `viewportRuler` and `dragDropRegistry` parameters
178+
* to be removed. Also `dragDrop` parameter to be made required.
179+
* @breaking-change 8.0.0.
180+
*/
181+
dragDrop?: DragDrop) {
182+
183+
184+
// @breaking-change 8.0.0 Remove null check once the paramter is made required.
185+
if (dragDrop) {
186+
this._dragRef = dragDrop.createDrag(element, config);
187+
} else {
188+
this._dragRef = new DragRef(element, config, _document, _ngZone, viewportRuler,
189+
dragDropRegistry);
190+
}
191+
192+
this._dragRef.data = this;
193+
this._syncInputs(this._dragRef);
194+
this._proxyEvents(this._dragRef);
182195
}
183196

184197
/**
@@ -273,15 +286,28 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
273286
private _syncInputs(ref: DragRef<CdkDrag<T>>) {
274287
ref.beforeStarted.subscribe(() => {
275288
if (!ref.isDragging()) {
276-
const {_placeholderTemplate: placeholder, _previewTemplate: preview} = this;
289+
const dir = this._dir;
290+
const placeholder = this._placeholderTemplate ? {
291+
template: this._placeholderTemplate.templateRef,
292+
context: this._placeholderTemplate.data,
293+
viewContainer: this._viewContainerRef
294+
} : null;
295+
const preview = this._previewTemplate ? {
296+
template: this._previewTemplate.templateRef,
297+
context: this._previewTemplate.data,
298+
viewContainer: this._viewContainerRef
299+
} : null;
277300

278301
ref.disabled = this.disabled;
279302
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);
303+
ref
304+
.withBoundaryElement(this._getBoundaryElement())
305+
.withPlaceholderTemplate(placeholder)
306+
.withPreviewTemplate(preview);
307+
308+
if (dir) {
309+
ref.withDirection(dir.value);
310+
}
285311
}
286312
});
287313
}

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

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ChangeDetectorRef,
2222
SkipSelf,
2323
Inject,
24+
AfterContentInit,
2425
} from '@angular/core';
2526
import {DOCUMENT} from '@angular/common';
2627
import {Directionality} from '@angular/cdk/bidi';
@@ -31,6 +32,9 @@ import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from '../drop-list-contai
3132
import {CdkDropListGroup} from './drop-list-group';
3233
import {DropListRef} from '../drop-list-ref';
3334
import {DragRef} from '../drag-ref';
35+
import {DragDrop} from '../drag-drop';
36+
import {Subject} from 'rxjs';
37+
import {startWith, takeUntil} from 'rxjs/operators';
3438

3539
/** Counter used to generate unique ids for drop zones. */
3640
let _uniqueIdCounter = 0;
@@ -62,15 +66,22 @@ export interface CdkDropListInternal extends CdkDropList {}
6266
'[class.cdk-drop-list-receiving]': '_dropListRef.isReceiving()',
6367
}
6468
})
65-
export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
69+
export class CdkDropList<T = any> implements CdkDropListContainer, AfterContentInit, OnDestroy {
70+
/** Emits when the list has been destroyed. */
71+
private _destroyed = new Subject<void>();
72+
6673
/** Keeps track of the drop lists that are currently on the page. */
6774
private static _dropLists: CdkDropList[] = [];
6875

6976
/** Reference to the underlying drop list instance. */
7077
_dropListRef: DropListRef<CdkDropList<T>>;
7178

7279
/** Draggable items in the container. */
73-
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;
80+
@ContentChildren(forwardRef(() => CdkDrag), {
81+
// Explicitly set to false since some of the logic below makes assumptions about it.
82+
// The `.withItems` call below should be updated if we ever need to switch this to `true`.
83+
descendants: false
84+
}) _draggables: QueryList<CdkDrag>;
7485

7586
/**
7687
* Other draggable containers that this container is connected to and into which the
@@ -134,34 +145,52 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
134145
sorted: EventEmitter<CdkDragSortEvent<T>> = new EventEmitter<CdkDragSortEvent<T>>();
135146

136147
constructor(
148+
/** Element that the drop list is attached to. */
137149
public element: ElementRef<HTMLElement>,
138150
dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
139151
private _changeDetectorRef: ChangeDetectorRef,
140-
@Optional() dir?: Directionality,
152+
@Optional() private _dir?: Directionality,
141153
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
142-
// @breaking-change 8.0.0 `_document` parameter to be made required.
143-
@Optional() @Inject(DOCUMENT) _document?: any) {
144-
154+
@Optional() @Inject(DOCUMENT) _document?: any,
155+
156+
/**
157+
* @deprecated `dragDropRegistry` and `_document` parameters to be removed.
158+
* Also `dragDrop` parameter to be made required.
159+
* @breaking-change 8.0.0.
160+
*/
161+
dragDrop?: DragDrop) {
162+
163+
// @breaking-change 8.0.0 Remove null check once `dragDrop` parameter is made required.
164+
if (dragDrop) {
165+
this._dropListRef = dragDrop.createDropList(element);
166+
} else {
167+
this._dropListRef = new DropListRef(element, dragDropRegistry, _document || document);
168+
}
145169

146-
// @breaking-change 8.0.0 Remove || once `_document` parameter is required.
147-
const ref = this._dropListRef = new DropListRef(element, dragDropRegistry,
148-
_document || document, dir);
149-
ref.data = this;
150-
ref.enterPredicate = (drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
170+
this._dropListRef.data = this;
171+
this._dropListRef.enterPredicate = (drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
151172
return this.enterPredicate(drag.data, drop.data);
152173
};
153-
this._syncInputs(ref);
154-
this._handleEvents(ref);
174+
175+
this._syncInputs(this._dropListRef);
176+
this._handleEvents(this._dropListRef);
155177
CdkDropList._dropLists.push(this);
156178

157179
if (_group) {
158180
_group._items.add(this);
159181
}
160182
}
161183

184+
ngAfterContentInit() {
185+
this._draggables.changes
186+
.pipe(startWith(this._draggables), takeUntil(this._destroyed))
187+
.subscribe((items: QueryList<CdkDrag>) => {
188+
this._dropListRef.withItems(items.map(drag => drag._dragRef));
189+
});
190+
}
191+
162192
ngOnDestroy() {
163193
const index = CdkDropList._dropLists.indexOf(this);
164-
this._dropListRef.dispose();
165194

166195
if (index > -1) {
167196
CdkDropList._dropLists.splice(index, 1);
@@ -170,6 +199,10 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
170199
if (this._group) {
171200
this._group._items.delete(this);
172201
}
202+
203+
this._dropListRef.dispose();
204+
this._destroyed.next();
205+
this._destroyed.complete();
173206
}
174207

175208
/** Starts dragging an item. */
@@ -253,6 +286,12 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
253286

254287
/** Syncs the inputs of the CdkDropList with the options of the underlying DropListRef. */
255288
private _syncInputs(ref: DropListRef<CdkDropList>) {
289+
if (this._dir) {
290+
this._dir.change
291+
.pipe(startWith(this._dir.value), takeUntil(this._destroyed))
292+
.subscribe(value => ref.withDirection(value));
293+
}
294+
256295
ref.beforeStarted.subscribe(() => {
257296
const siblings = coerceArray(this.connectedTo).map(drop => {
258297
return typeof drop === 'string' ?
@@ -270,8 +309,7 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
270309
ref.lockAxis = this.lockAxis;
271310
ref
272311
.connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef))
273-
.withOrientation(this.orientation)
274-
.withItems(this._draggables.map(drag => drag._dragRef));
312+
.withOrientation(this.orientation);
275313
});
276314
}
277315

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {CdkDrag} from './directives/drag';
1313
import {CdkDragHandle} from './directives/drag-handle';
1414
import {CdkDragPreview} from './directives/drag-preview';
1515
import {CdkDragPlaceholder} from './directives/drag-placeholder';
16+
import {DragDrop} from './drag-drop';
1617

1718
@NgModule({
1819
declarations: [
@@ -31,5 +32,8 @@ import {CdkDragPlaceholder} from './directives/drag-placeholder';
3132
CdkDragPreview,
3233
CdkDragPlaceholder,
3334
],
35+
providers: [
36+
DragDrop,
37+
]
3438
})
3539
export class DragDropModule {}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {Component, ElementRef} from '@angular/core';
2+
import {fakeAsync, TestBed, inject} from '@angular/core/testing';
3+
import {DragDropModule} from './drag-drop-module';
4+
import {DragDrop} from './drag-drop';
5+
import {DragRef} from './drag-ref';
6+
import {DropListRef} from './drop-list-ref';
7+
8+
describe('DragDrop', () => {
9+
let service: DragDrop;
10+
11+
beforeEach(fakeAsync(() => {
12+
TestBed.configureTestingModule({
13+
declarations: [TestComponent],
14+
imports: [DragDropModule],
15+
});
16+
17+
TestBed.compileComponents();
18+
}));
19+
20+
beforeEach(inject([DragDrop], (d: DragDrop) => {
21+
service = d;
22+
}));
23+
24+
it('should be able to attach a DragRef to a DOM node', () => {
25+
const fixture = TestBed.createComponent(TestComponent);
26+
fixture.detectChanges();
27+
const ref = service.createDrag(fixture.componentInstance.elementRef);
28+
29+
expect(ref instanceof DragRef).toBe(true);
30+
});
31+
32+
it('should be able to attach a DropListRef to a DOM node', () => {
33+
const fixture = TestBed.createComponent(TestComponent);
34+
fixture.detectChanges();
35+
const ref = service.createDropList(fixture.componentInstance.elementRef);
36+
37+
expect(ref instanceof DropListRef).toBe(true);
38+
});
39+
});
40+
41+
42+
@Component({
43+
template: '<div></div>'
44+
})
45+
class TestComponent {
46+
constructor(public elementRef: ElementRef<HTMLElement>) {}
47+
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Injectable, Inject, NgZone, ElementRef} from '@angular/core';
10+
import {DOCUMENT} from '@angular/common';
11+
import {ViewportRuler} from '@angular/cdk/scrolling';
12+
import {DragRef, DragRefConfig} from './drag-ref';
13+
import {DropListRef} from './drop-list-ref';
14+
import {DragDropRegistry} from './drag-drop-registry';
15+
16+
/** Default configuration to be used when creating a `DragRef`. */
17+
const DEFAULT_CONFIG = {
18+
dragStartThreshold: 5,
19+
pointerDirectionChangeThreshold: 5
20+
};
21+
22+
/**
23+
* Service that allows for drag-and-drop functionality to be attached to DOM elements.
24+
*/
25+
@Injectable({providedIn: 'root'})
26+
export class DragDrop {
27+
constructor(
28+
@Inject(DOCUMENT) private _document: any,
29+
private _ngZone: NgZone,
30+
private _viewportRuler: ViewportRuler,
31+
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>) {}
32+
33+
/**
34+
* Turns an element into a draggable item.
35+
* @param element Element to which to attach the dragging functionality.
36+
* @param config Object used to configure the dragging behavior.
37+
*/
38+
createDrag<T = any>(element: ElementRef<HTMLElement> | HTMLElement,
39+
config: DragRefConfig = DEFAULT_CONFIG): DragRef<T> {
40+
41+
return new DragRef<T>(element, config, this._document, this._ngZone, this._viewportRuler,
42+
this._dragDropRegistry);
43+
}
44+
45+
/**
46+
* Turns an element into a drop list.
47+
* @param element Element to which to attach the drop list functionality.
48+
*/
49+
createDropList<T = any>(element: ElementRef<HTMLElement> | HTMLElement): DropListRef<T> {
50+
return new DropListRef<T>(element, this._dragDropRegistry, this._document);
51+
}
52+
}

0 commit comments

Comments
 (0)