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.
This commit is contained in:
Kristiyan Kostadinov
2018-11-10 00:42:44 +00:00
committed by vivian-hu
parent 3842c8c633
commit 962dbeb1a6
7 changed files with 145 additions and 17 deletions
+3
View File
@@ -8,6 +8,7 @@
import {NgModule} from '@angular/core';
import {CdkDropList} from './drop-list';
import {CdkDropListGroup} from './drop-list-group';
import {CdkDrag} from './drag';
import {CdkDragHandle} from './drag-handle';
import {CdkDragPreview} from './drag-preview';
@@ -16,6 +17,7 @@ import {CdkDragPlaceholder} from './drag-placeholder';
@NgModule({
declarations: [
CdkDropList,
CdkDropListGroup,
CdkDrag,
CdkDragHandle,
CdkDragPreview,
@@ -23,6 +25,7 @@ import {CdkDragPlaceholder} from './drag-placeholder';
],
exports: [
CdkDropList,
CdkDropListGroup,
CdkDrag,
CdkDragHandle,
CdkDragPreview,
+16 -3
View File
@@ -22,12 +22,12 @@ update the data model once the user finishes dragging.
### Transferring items between lists
The `cdkDropList` directive supports transferring dragged items between connected drop zones.
You can connect one or more `cdkDropList` instances together by setting the `cdkDropListConnectedTo`
property.
property or by wrapping the elements in an element with the `cdkDropListGroup` attribute.
<!-- example(cdk-drag-drop-connected-sorting) -->
Note that `cdkDropListConnectedTo` works both with a direct reference to another `cdkDropList`, or by
referencing the `id` of another drop container:
Note that `cdkDropListConnectedTo` works both with a direct reference to another `cdkDropList`, or
by referencing the `id` of another drop container:
```html
<!-- This is valid -->
@@ -39,6 +39,19 @@ referencing the `id` of another drop container:
<div cdkDropList id="list-two" [cdkDropListConnectedTo]="['list-one']"></div>
```
If you have an unknown number of connected drop lists, you can use the `cdkDropListGroup` directive
to set up the connection automatically. Note that any new `cdkDropList` that is added under a group
will be connected to all other automatically.
```html
<div cdkDropListGroup>
<!-- All lists in here will be connected. -->
<div cdkDropList *ngFor="let list of lists"></div>
</div>
```
<!-- example(cdk-drag-drop-connected-sorting-group) -->
### Attaching data
You can associate some arbitrary data with both `cdkDrag` and `cdkDropList` by setting `cdkDragData`
or `cdkDropListData`, respectively. Events fired from both directives include this data, allowing
+61
View File
@@ -1927,6 +1927,31 @@ describe('CdkDrag', () => {
});
}));
it('should be able to connect two drop zones using the drop list group', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZonesViaGroupDirective);
fixture.detectChanges();
const dropInstances = fixture.componentInstance.dropInstances.toArray();
const groups = fixture.componentInstance.groupedDragItems;
const element = groups[0][1].element.nativeElement;
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
flush();
fixture.detectChanges();
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
expect(event).toBeTruthy();
expect(event).toEqual({
previousIndex: 1,
currentIndex: 3,
item: groups[0][1],
container: dropInstances[1],
previousContainer: dropInstances[0]
});
}));
it('should be able to pass a single id to `connectedTo`', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();
@@ -2311,6 +2336,42 @@ class ConnectedDropZones implements AfterViewInit {
}
}
@Component({
encapsulation: ViewEncapsulation.None,
styles: [`
.cdk-drop-list {
display: block;
width: 100px;
min-height: ${ITEM_HEIGHT}px;
background: hotpink;
}
.cdk-drag {
display: block;
height: ${ITEM_HEIGHT}px;
background: red;
}
`],
template: `
<div cdkDropListGroup>
<div
cdkDropList
[cdkDropListData]="todo"
(cdkDropListDropped)="droppedSpy($event)">
<div [cdkDragData]="item" *ngFor="let item of todo" cdkDrag>{{item}}</div>
</div>
<div
cdkDropList
[cdkDropListData]="done"
(cdkDropListDropped)="droppedSpy($event)">
<div [cdkDragData]="item" *ngFor="let item of done" cdkDrag>{{item}}</div>
</div>
</div>
`
})
class ConnectedDropZonesViaGroupDirective extends ConnectedDropZones {}
@Component({
template: `
+27
View File
@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, OnDestroy} from '@angular/core';
/**
* Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList`
* elements that are placed inside a `cdkDropListGroup` will be connected to each other
* automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input
* from `cdkDropList`.
*/
@Directive({
selector: '[cdkDropListGroup]'
})
export class CdkDropListGroup<T> implements OnDestroy {
/** Drop lists registered inside the group. */
readonly _items = new Set<T>();
ngOnDestroy() {
this._items.clear();
}
}
+34 -7
View File
@@ -27,6 +27,7 @@ import {DragDropRegistry} from './drag-drop-registry';
import {CdkDragDrop, CdkDragEnter, CdkDragExit} from './drag-events';
import {moveItemInArray} from './drag-utils';
import {CDK_DROP_LIST_CONTAINER} from './drop-list-container';
import {CdkDropListGroup} from './drop-list-group';
/** Counter used to generate unique ids for drop zones. */
@@ -143,14 +144,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
public element: ElementRef<HTMLElement>,
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir?: Directionality) {}
@Optional() private _dir?: Directionality,
@Optional() private _group?: CdkDropListGroup<CdkDropList>) {}
ngOnInit() {
this._dragDropRegistry.registerDropContainer(this);
if (this._group) {
this._group._items.add(this);
}
}
ngOnDestroy() {
this._dragDropRegistry.removeDropContainer(this);
if (this._group) {
this._group._items.delete(this);
}
}
/** Whether an item in the container is being dragged. */
@@ -366,6 +376,8 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
/** Refreshes the position cache of the items and sibling containers. */
private _cachePositions() {
const isHorizontal = this.orientation === 'horizontal';
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
this._positionCache.items = this._activeDraggables
.map(drag => {
const elementToMeasure = this._dragDropRegistry.isDragging(drag) ?
@@ -397,12 +409,10 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
a.clientRect.top - b.clientRect.top;
});
this._positionCache.siblings = coerceArray(this.connectedTo)
.map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop)
.filter(drop => drop && drop !== this)
.map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()}));
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
this._positionCache.siblings = this._getConnectedLists().map(drop => ({
drop,
clientRect: drop.element.nativeElement.getBoundingClientRect()
}));
}
/** Resets the container to its initial state. */
@@ -535,6 +545,23 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
return siblingOffset;
}
/** Gets an array of unique drop lists that the current list is connected to. */
private _getConnectedLists(): CdkDropList[] {
const siblings = coerceArray(this.connectedTo).map(drop => {
return typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop;
});
if (this._group) {
this._group._items.forEach(drop => {
if (siblings.indexOf(drop) === -1) {
siblings.push(drop);
}
});
}
return siblings.filter(drop => drop && drop !== this);
}
}
+1
View File
@@ -7,6 +7,7 @@
*/
export * from './drop-list';
export * from './drop-list-group';
export * from './drop-list-container';
export * from './drag';
export * from './drag-handle';
+3 -7
View File
@@ -1,4 +1,4 @@
<div>
<div cdkDropListGroup>
<div class="demo-list">
<h2>To do</h2>
@@ -7,11 +7,9 @@
</mat-slide-toggle>
<div
cdkDropList
#one="cdkDropList"
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[two]">
[cdkDropListData]="todo">
<div *ngFor="let item of todo" cdkDrag>
{{item}}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
@@ -23,11 +21,9 @@
<h2>Done</h2>
<div
cdkDropList
#two="cdkDropList"
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[one]">
[cdkDropListData]="done">
<div *ngFor="let item of done" cdkDrag>
{{item}}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>