Skip to content

Commit 4515121

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 47de296 commit 4515121

File tree

7 files changed

+146
-18
lines changed

7 files changed

+146
-18
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 amount 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
@@ -1600,6 +1600,31 @@ describe('CdkDrag', () => {
16001600
});
16011601
}));
16021602

1603+
it('should be able to connect two drop zones using the drop list group', fakeAsync(() => {
1604+
const fixture = createComponent(ConnectedDropZonesViaGroupDirective);
1605+
fixture.detectChanges();
1606+
1607+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
1608+
const groups = fixture.componentInstance.groupedDragItems;
1609+
const element = groups[0][1].element.nativeElement;
1610+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
1611+
1612+
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
1613+
flush();
1614+
fixture.detectChanges();
1615+
1616+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
1617+
1618+
expect(event).toBeTruthy();
1619+
expect(event).toEqual({
1620+
previousIndex: 1,
1621+
currentIndex: 3,
1622+
item: groups[0][1],
1623+
container: dropInstances[1],
1624+
previousContainer: dropInstances[0]
1625+
});
1626+
}));
1627+
16031628
it('should be able to pass a single id to `connectedTo`', fakeAsync(() => {
16041629
const fixture = createComponent(ConnectedDropZones);
16051630
fixture.detectChanges();
@@ -1963,6 +1988,42 @@ class ConnectedDropZones implements AfterViewInit {
19631988
}
19641989
}
19651990

1991+
@Component({
1992+
encapsulation: ViewEncapsulation.None,
1993+
styles: [`
1994+
.cdk-drop-list {
1995+
display: block;
1996+
width: 100px;
1997+
min-height: ${ITEM_HEIGHT}px;
1998+
background: hotpink;
1999+
}
2000+
2001+
.cdk-drag {
2002+
display: block;
2003+
height: ${ITEM_HEIGHT}px;
2004+
background: red;
2005+
}
2006+
`],
2007+
template: `
2008+
<div cdkDropListGroup>
2009+
<div
2010+
cdkDropList
2011+
[cdkDropListData]="todo"
2012+
(cdkDropListDropped)="droppedSpy($event)">
2013+
<div [cdkDragData]="item" *ngFor="let item of todo" cdkDrag>{{item}}</div>
2014+
</div>
2015+
2016+
<div
2017+
cdkDropList
2018+
[cdkDropListData]="done"
2019+
(cdkDropListDropped)="droppedSpy($event)">
2020+
<div [cdkDragData]="item" *ngFor="let item of done" cdkDrag>{{item}}</div>
2021+
</div>
2022+
</div>
2023+
`
2024+
})
2025+
class ConnectedDropZonesViaGroupDirective extends ConnectedDropZones {}
2026+
19662027

19672028
@Component({
19682029
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: 35 additions & 8 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. */
@@ -104,21 +105,30 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
104105
constructor(
105106
public element: ElementRef<HTMLElement>,
106107
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
107-
@Optional() private _dir?: Directionality) {}
108+
@Optional() private _dir?: Directionality,
109+
@Optional() private _group?: CdkDropListGroup<CdkDropList>) {}
108110

109111
ngOnInit() {
110112
this._dragDropRegistry.registerDropContainer(this);
113+
114+
if (this._group) {
115+
this._group._items.add(this);
116+
}
111117
}
112118

113119
ngOnDestroy() {
114120
this._dragDropRegistry.removeDropContainer(this);
121+
122+
if (this._group) {
123+
this._group._items.delete(this);
124+
}
115125
}
116126

117127
/** Whether an item in the container is being dragged. */
118128
_dragging = false;
119129

120130
/** Cache of the dimensions of all the items and the sibling containers. */
121-
private _positionCache = {
131+
private readonly _positionCache = {
122132
items: [] as {drag: CdkDrag, clientRect: ClientRect, offset: number}[],
123133
siblings: [] as {drop: CdkDropList, clientRect: ClientRect}[],
124134
self: {} as ClientRect
@@ -333,6 +343,8 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
333343
/** Refreshes the position cache of the items and sibling containers. */
334344
private _cachePositions() {
335345
const isHorizontal = this.orientation === 'horizontal';
346+
347+
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
336348
this._positionCache.items = this._activeDraggables
337349
.map(drag => {
338350
const elementToMeasure = this._dragDropRegistry.isDragging(drag) ?
@@ -364,12 +376,10 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
364376
a.clientRect.top - b.clientRect.top;
365377
});
366378

367-
this._positionCache.siblings = coerceArray(this.connectedTo)
368-
.map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop)
369-
.filter(drop => drop && drop !== this)
370-
.map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()}));
371-
372-
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
379+
this._positionCache.siblings = this._getConnectedLists().map(drop => ({
380+
drop,
381+
clientRect: drop.element.nativeElement.getBoundingClientRect()
382+
}));
373383
}
374384

375385
/** Resets the container to its initial state. */
@@ -449,6 +459,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
449459
return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
450460
pointerX > left - xThreshold && pointerX < right + xThreshold;
451461
}
462+
463+
/** Gets an array of unique drop lists that the current list is connected to. */
464+
private _getConnectedLists(): CdkDropList[] {
465+
const siblings = coerceArray(this.connectedTo).map(drop => {
466+
return typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop;
467+
});
468+
469+
if (this._group) {
470+
this._group._items.forEach(drop => {
471+
if (siblings.indexOf(drop) === -1) {
472+
siblings.push(drop);
473+
}
474+
});
475+
}
476+
477+
return siblings.filter(drop => drop && drop !== this);
478+
}
452479
}
453480

454481

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,13 +1,11 @@
1-
<div>
1+
<div cdkDropListGroup>
22
<div class="demo-list">
33
<h2>To do</h2>
44
<div
55
cdkDropList
6-
#one="cdkDropList"
76
(cdkDropListDropped)="drop($event)"
87
[cdkDropListLockAxis]="axisLock"
9-
[cdkDropListData]="todo"
10-
[cdkDropListConnectedTo]="[two]">
8+
[cdkDropListData]="todo">
119
<div *ngFor="let item of todo" cdkDrag>
1210
{{item}}
1311
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
@@ -19,11 +17,9 @@ <h2>To do</h2>
1917
<h2>Done</h2>
2018
<div
2119
cdkDropList
22-
#two="cdkDropList"
2320
(cdkDropListDropped)="drop($event)"
2421
[cdkDropListLockAxis]="axisLock"
25-
[cdkDropListData]="done"
26-
[cdkDropListConnectedTo]="[one]">
22+
[cdkDropListData]="done">
2723
<div *ngFor="let item of done" cdkDrag>
2824
{{item}}
2925
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>

0 commit comments

Comments
 (0)