Skip to content

Commit 10d06f7

Browse files
committed
feat(drag-drop): add directive to connect drop lists automatically
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 76044e8 commit 10d06f7

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
@@ -1784,6 +1784,31 @@ describe('CdkDrag', () => {
17841784
});
17851785
}));
17861786

1787+
it('should be able to connect two drop zones using the drop list group', fakeAsync(() => {
1788+
const fixture = createComponent(ConnectedDropZonesViaGroupDirective);
1789+
fixture.detectChanges();
1790+
1791+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
1792+
const groups = fixture.componentInstance.groupedDragItems;
1793+
const element = groups[0][1].element.nativeElement;
1794+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
1795+
1796+
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
1797+
flush();
1798+
fixture.detectChanges();
1799+
1800+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
1801+
1802+
expect(event).toBeTruthy();
1803+
expect(event).toEqual({
1804+
previousIndex: 1,
1805+
currentIndex: 3,
1806+
item: groups[0][1],
1807+
container: dropInstances[1],
1808+
previousContainer: dropInstances[0]
1809+
});
1810+
}));
1811+
17871812
it('should be able to pass a single id to `connectedTo`', fakeAsync(() => {
17881813
const fixture = createComponent(ConnectedDropZones);
17891814
fixture.detectChanges();
@@ -2162,6 +2187,42 @@ class ConnectedDropZones implements AfterViewInit {
21622187
}
21632188
}
21642189

2190+
@Component({
2191+
encapsulation: ViewEncapsulation.None,
2192+
styles: [`
2193+
.cdk-drop-list {
2194+
display: block;
2195+
width: 100px;
2196+
min-height: ${ITEM_HEIGHT}px;
2197+
background: hotpink;
2198+
}
2199+
2200+
.cdk-drag {
2201+
display: block;
2202+
height: ${ITEM_HEIGHT}px;
2203+
background: red;
2204+
}
2205+
`],
2206+
template: `
2207+
<div cdkDropListGroup>
2208+
<div
2209+
cdkDropList
2210+
[cdkDropListData]="todo"
2211+
(cdkDropListDropped)="droppedSpy($event)">
2212+
<div [cdkDragData]="item" *ngFor="let item of todo" cdkDrag>{{item}}</div>
2213+
</div>
2214+
2215+
<div
2216+
cdkDropList
2217+
[cdkDropListData]="done"
2218+
(cdkDropListDropped)="droppedSpy($event)">
2219+
<div [cdkDragData]="item" *ngFor="let item of done" cdkDrag>{{item}}</div>
2220+
</div>
2221+
</div>
2222+
`
2223+
})
2224+
class ConnectedDropZonesViaGroupDirective extends ConnectedDropZones {}
2225+
21652226

21662227
@Component({
21672228
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
@@ -26,6 +26,7 @@ import {DragDropRegistry} from './drag-drop-registry';
2626
import {CdkDragDrop, CdkDragEnter, CdkDragExit} from './drag-events';
2727
import {moveItemInArray} from './drag-utils';
2828
import {CDK_DROP_LIST_CONTAINER} from './drop-list-container';
29+
import {CdkDropListGroup} from './drop-list-group';
2930

3031

3132
/** Counter used to generate unique ids for drop zones. */
@@ -141,14 +142,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
141142
constructor(
142143
public element: ElementRef<HTMLElement>,
143144
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
144-
@Optional() private _dir?: Directionality) {}
145+
@Optional() private _dir?: Directionality,
146+
@Optional() private _group?: CdkDropListGroup<CdkDropList>) {}
145147

146148
ngOnInit() {
147149
this._dragDropRegistry.registerDropContainer(this);
150+
151+
if (this._group) {
152+
this._group._items.add(this);
153+
}
148154
}
149155

150156
ngOnDestroy() {
151157
this._dragDropRegistry.removeDropContainer(this);
158+
159+
if (this._group) {
160+
this._group._items.delete(this);
161+
}
152162
}
153163

154164
/** Whether an item in the container is being dragged. */
@@ -364,6 +374,8 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
364374
/** Refreshes the position cache of the items and sibling containers. */
365375
private _cachePositions() {
366376
const isHorizontal = this.orientation === 'horizontal';
377+
378+
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
367379
this._positionCache.items = this._activeDraggables
368380
.map(drag => {
369381
const elementToMeasure = this._dragDropRegistry.isDragging(drag) ?
@@ -395,12 +407,10 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
395407
a.clientRect.top - b.clientRect.top;
396408
});
397409

398-
this._positionCache.siblings = coerceArray(this.connectedTo)
399-
.map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop)
400-
.filter(drop => drop && drop !== this)
401-
.map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()}));
402-
403-
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
410+
this._positionCache.siblings = this._getConnectedLists().map(drop => ({
411+
drop,
412+
clientRect: drop.element.nativeElement.getBoundingClientRect()
413+
}));
404414
}
405415

406416
/** Resets the container to its initial state. */
@@ -533,6 +543,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
533543

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

538565

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)