Skip to content

Commit e905127

Browse files
crisbetoVivian Hu
authored andcommitted
feat(drag-drop): add directive to connect drop lists automatically (#13754)
Currently the only way to connect drop lists is via the `cdkDropListConnectedTo` input which works if the consumer knows the amount of drop lists that they're going to have. For dynamic lists they can pass a list of ids, however that can be a little boilerplate-y. These changes introduce the `cdkDropListGroup` directive which can be used to connect a dynamic list of drop containers automatically. Fixes #13750.
1 parent f5006d6 commit e905127

File tree

7 files changed

+145
-17
lines changed

7 files changed

+145
-17
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {NgModule} from '@angular/core';
1010
import {CdkDropList} from './drop-list';
11+
import {CdkDropListGroup} from './drop-list-group';
1112
import {CdkDrag} from './drag';
1213
import {CdkDragHandle} from './drag-handle';
1314
import {CdkDragPreview} from './drag-preview';
@@ -16,13 +17,15 @@ import {CdkDragPlaceholder} from './drag-placeholder';
1617
@NgModule({
1718
declarations: [
1819
CdkDropList,
20+
CdkDropListGroup,
1921
CdkDrag,
2022
CdkDragHandle,
2123
CdkDragPreview,
2224
CdkDragPlaceholder,
2325
],
2426
exports: [
2527
CdkDropList,
28+
CdkDropListGroup,
2629
CdkDrag,
2730
CdkDragHandle,
2831
CdkDragPreview,

src/cdk/drag-drop/drag-drop.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ update the data model once the user finishes dragging.
2222
### Transferring items between lists
2323
The `cdkDropList` directive supports transferring dragged items between connected drop zones.
2424
You can connect one or more `cdkDropList` instances together by setting the `cdkDropListConnectedTo`
25-
property.
25+
property or by wrapping the elements in an element with the `cdkDropListGroup` attribute.
2626

2727
<!-- example(cdk-drag-drop-connected-sorting) -->
2828

29-
Note that `cdkDropListConnectedTo` works both with a direct reference to another `cdkDropList`, or by
30-
referencing the `id` of another drop container:
29+
Note that `cdkDropListConnectedTo` works both with a direct reference to another `cdkDropList`, or
30+
by referencing the `id` of another drop container:
3131

3232
```html
3333
<!-- This is valid -->
@@ -39,6 +39,19 @@ referencing the `id` of another drop container:
3939
<div cdkDropList id="list-two" [cdkDropListConnectedTo]="['list-one']"></div>
4040
```
4141

42+
If you have an unknown number of connected drop lists, you can use the `cdkDropListGroup` directive
43+
to set up the connection automatically. Note that any new `cdkDropList` that is added under a group
44+
will be connected to all other automatically.
45+
46+
```html
47+
<div cdkDropListGroup>
48+
<!-- All lists in here will be connected. -->
49+
<div cdkDropList *ngFor="let list of lists"></div>
50+
</div>
51+
```
52+
53+
<!-- example(cdk-drag-drop-connected-sorting-group) -->
54+
4255
### Attaching data
4356
You can associate some arbitrary data with both `cdkDrag` and `cdkDropList` by setting `cdkDragData`
4457
or `cdkDropListData`, respectively. Events fired from both directives include this data, allowing

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,6 +1927,31 @@ describe('CdkDrag', () => {
19271927
});
19281928
}));
19291929

1930+
it('should be able to connect two drop zones using the drop list group', fakeAsync(() => {
1931+
const fixture = createComponent(ConnectedDropZonesViaGroupDirective);
1932+
fixture.detectChanges();
1933+
1934+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
1935+
const groups = fixture.componentInstance.groupedDragItems;
1936+
const element = groups[0][1].element.nativeElement;
1937+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
1938+
1939+
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
1940+
flush();
1941+
fixture.detectChanges();
1942+
1943+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
1944+
1945+
expect(event).toBeTruthy();
1946+
expect(event).toEqual({
1947+
previousIndex: 1,
1948+
currentIndex: 3,
1949+
item: groups[0][1],
1950+
container: dropInstances[1],
1951+
previousContainer: dropInstances[0]
1952+
});
1953+
}));
1954+
19301955
it('should be able to pass a single id to `connectedTo`', fakeAsync(() => {
19311956
const fixture = createComponent(ConnectedDropZones);
19321957
fixture.detectChanges();
@@ -2311,6 +2336,42 @@ class ConnectedDropZones implements AfterViewInit {
23112336
}
23122337
}
23132338

2339+
@Component({
2340+
encapsulation: ViewEncapsulation.None,
2341+
styles: [`
2342+
.cdk-drop-list {
2343+
display: block;
2344+
width: 100px;
2345+
min-height: ${ITEM_HEIGHT}px;
2346+
background: hotpink;
2347+
}
2348+
2349+
.cdk-drag {
2350+
display: block;
2351+
height: ${ITEM_HEIGHT}px;
2352+
background: red;
2353+
}
2354+
`],
2355+
template: `
2356+
<div cdkDropListGroup>
2357+
<div
2358+
cdkDropList
2359+
[cdkDropListData]="todo"
2360+
(cdkDropListDropped)="droppedSpy($event)">
2361+
<div [cdkDragData]="item" *ngFor="let item of todo" cdkDrag>{{item}}</div>
2362+
</div>
2363+
2364+
<div
2365+
cdkDropList
2366+
[cdkDropListData]="done"
2367+
(cdkDropListDropped)="droppedSpy($event)">
2368+
<div [cdkDragData]="item" *ngFor="let item of done" cdkDrag>{{item}}</div>
2369+
</div>
2370+
</div>
2371+
`
2372+
})
2373+
class ConnectedDropZonesViaGroupDirective extends ConnectedDropZones {}
2374+
23142375

23152376
@Component({
23162377
template: `

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 {Directive, OnDestroy} from '@angular/core';
10+
11+
/**
12+
* Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`
13+
* elements that are placed inside a `cdkDropListGroup` will be connected to each other
14+
* automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input
15+
* from `cdkDropList`.
16+
*/
17+
@Directive({
18+
selector: '[cdkDropListGroup]'
19+
})
20+
export class CdkDropListGroup<T> implements OnDestroy {
21+
/** Drop lists registered inside the group. */
22+
readonly _items = new Set<T>();
23+
24+
ngOnDestroy() {
25+
this._items.clear();
26+
}
27+
}

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {DragDropRegistry} from './drag-drop-registry';
2727
import {CdkDragDrop, CdkDragEnter, CdkDragExit} from './drag-events';
2828
import {moveItemInArray} from './drag-utils';
2929
import {CDK_DROP_LIST_CONTAINER} from './drop-list-container';
30+
import {CdkDropListGroup} from './drop-list-group';
3031

3132

3233
/** Counter used to generate unique ids for drop zones. */
@@ -143,14 +144,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
143144
public element: ElementRef<HTMLElement>,
144145
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
145146
private _changeDetectorRef: ChangeDetectorRef,
146-
@Optional() private _dir?: Directionality) {}
147+
@Optional() private _dir?: Directionality,
148+
@Optional() private _group?: CdkDropListGroup<CdkDropList>) {}
147149

148150
ngOnInit() {
149151
this._dragDropRegistry.registerDropContainer(this);
152+
153+
if (this._group) {
154+
this._group._items.add(this);
155+
}
150156
}
151157

152158
ngOnDestroy() {
153159
this._dragDropRegistry.removeDropContainer(this);
160+
161+
if (this._group) {
162+
this._group._items.delete(this);
163+
}
154164
}
155165

156166
/** Whether an item in the container is being dragged. */
@@ -366,6 +376,8 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
366376
/** Refreshes the position cache of the items and sibling containers. */
367377
private _cachePositions() {
368378
const isHorizontal = this.orientation === 'horizontal';
379+
380+
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
369381
this._positionCache.items = this._activeDraggables
370382
.map(drag => {
371383
const elementToMeasure = this._dragDropRegistry.isDragging(drag) ?
@@ -397,12 +409,10 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
397409
a.clientRect.top - b.clientRect.top;
398410
});
399411

400-
this._positionCache.siblings = coerceArray(this.connectedTo)
401-
.map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop)
402-
.filter(drop => drop && drop !== this)
403-
.map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()}));
404-
405-
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
412+
this._positionCache.siblings = this._getConnectedLists().map(drop => ({
413+
drop,
414+
clientRect: drop.element.nativeElement.getBoundingClientRect()
415+
}));
406416
}
407417

408418
/** Resets the container to its initial state. */
@@ -535,6 +545,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
535545

536546
return siblingOffset;
537547
}
548+
549+
/** Gets an array of unique drop lists that the current list is connected to. */
550+
private _getConnectedLists(): CdkDropList[] {
551+
const siblings = coerceArray(this.connectedTo).map(drop => {
552+
return typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop;
553+
});
554+
555+
if (this._group) {
556+
this._group._items.forEach(drop => {
557+
if (siblings.indexOf(drop) === -1) {
558+
siblings.push(drop);
559+
}
560+
});
561+
}
562+
563+
return siblings.filter(drop => drop && drop !== this);
564+
}
538565
}
539566

540567

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export * from './drop-list';
10+
export * from './drop-list-group';
1011
export * from './drop-list-container';
1112
export * from './drag';
1213
export * from './drag-handle';

src/demo-app/drag-drop/drag-drop-demo.html

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div>
1+
<div cdkDropListGroup>
22
<div class="demo-list">
33

44
<h2>To do</h2>
@@ -7,11 +7,9 @@ <h2>To do</h2>
77
</mat-slide-toggle>
88
<div
99
cdkDropList
10-
#one="cdkDropList"
1110
(cdkDropListDropped)="drop($event)"
1211
[cdkDropListLockAxis]="axisLock"
13-
[cdkDropListData]="todo"
14-
[cdkDropListConnectedTo]="[two]">
12+
[cdkDropListData]="todo">
1513
<div *ngFor="let item of todo" cdkDrag>
1614
{{item}}
1715
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
@@ -23,11 +21,9 @@ <h2>To do</h2>
2321
<h2>Done</h2>
2422
<div
2523
cdkDropList
26-
#two="cdkDropList"
2724
(cdkDropListDropped)="drop($event)"
2825
[cdkDropListLockAxis]="axisLock"
29-
[cdkDropListData]="done"
30-
[cdkDropListConnectedTo]="[one]">
26+
[cdkDropListData]="done">
3127
<div *ngFor="let item of done" cdkDrag>
3228
{{item}}
3329
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>

0 commit comments

Comments
 (0)