Skip to content

feat(drag-drop): add directive to connect drop lists automatically #13754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/cdk/drag-drop/drag-drop-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,13 +17,15 @@ import {CdkDragPlaceholder} from './drag-placeholder';
@NgModule({
declarations: [
CdkDropList,
CdkDropListGroup,
CdkDrag,
CdkDragHandle,
CdkDragPreview,
CdkDragPlaceholder,
],
exports: [
CdkDropList,
CdkDropListGroup,
CdkDrag,
CdkDragHandle,
CdkDragPreview,
Expand Down
19 changes: 16 additions & 3 deletions src/cdk/drag-drop/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand All @@ -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
Expand Down
61 changes: 61 additions & 0 deletions src/cdk/drag-drop/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,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();
Expand Down Expand Up @@ -2246,6 +2271,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: `
Expand Down
27 changes: 27 additions & 0 deletions src/cdk/drag-drop/drop-list-group.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
41 changes: 34 additions & 7 deletions src/cdk/drag-drop/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was debating whether this should live in the CdkDropRegistry. I decided not to do it, because it was adding a fair bit of code for no real benefit.

}
}

ngOnDestroy() {
this._dragDropRegistry.removeDropContainer(this);

if (this._group) {
this._group._items.delete(this);
}
}

/** Whether an item in the container is being dragged. */
Expand Down Expand Up @@ -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) ?
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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);
}
}


Expand Down
1 change: 1 addition & 0 deletions src/cdk/drag-drop/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 3 additions & 7 deletions src/demo-app/drag-drop/drag-drop-demo.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div>
<div cdkDropListGroup>
<div class="demo-list">

<h2>To do</h2>
Expand All @@ -7,11 +7,9 @@ <h2>To do</h2>
</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>
Expand All @@ -23,11 +21,9 @@ <h2>To do</h2>
<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>
Expand Down