Search code examples
angularangular-materialangular-cdk-drag-drop

Angular Drag and Drop using Observable as datasource


I want to use an Observable as the data source for a material drag and drop, but I'm struggling at the moment. Basically I have a list of swimlanes in a kanban board and each swimlane has a bunch of items that I want to move around. When I move an item I want to update the back end as well. Multiple people will also be updating the same kanban board, so I want to use socket.io to automatically move the items around too (which I'm also struggling with for the same reason)

Here's the template:

<button mat-raised-button color="primary" (click)="addItem()">Add Item</button>

<button mat-raised-button color="primary" (click)="moveItem()" style="margin-left:10px">Move Item</button>

<div cdkDropListGroup>
<div class="example-container" *ngFor="let lane of swimlanes">
  <h2>{{lane.name}}</h2>

  <div
    cdkDropList
    #doneList="cdkDropList"
    [cdkDropListData]="lane.array | async"
    class="example-list"
    (cdkDropListDropped)="drop($event)">
    <div class="example-box" *ngFor="let item of lane.array | async" cdkDrag>{{item.name}}</div>
  </div>
</div>

</div>

Here's the component code:

import {Component, OnInit} from '@angular/core';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
import { Subscription, of, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Swimlane{
  name: string;
  array: Observable<Task[]>;
}

export interface Task{
  name: string;
  list: string;
}

/**
 * @title Drag&Drop connected sorting
 */
@Component({
  selector: 'cdk-drag-drop-connected-sorting-example',
  templateUrl: 'cdk-drag-drop-connected-sorting-example.html',
  styleUrls: ['cdk-drag-drop-connected-sorting-example.css'],
})
export class CdkDragDropConnectedSortingExample implements OnInit {

//These will eventually come from an API.
  items:Task[] = [
    {name: "Get to work", list: "To Do"},
    {name: "Pick up groceries", list: "To Do"},
    {name: "Go home", list: "To Do"},
    {name: "Fall asleep", list: "To Do"},
    {name: "Get up", list: "Done"},
    {name: "Brush teeth", list: "Done"},
    {name: "Take a shower", list: "Done"},
    {name: "Check e-mail", list: "Done"},
    {name: "Walk dog", list: "Done"}
  ];


  swimlanes:Swimlane[] = [];

//Create Observables as the data sources
  itemsObservable:Observable<Task[]>;

  todo:Observable<Task[]>;
  done:Observable<Task[]>;

  ngOnInit(): void {

    this.itemsObservable = of(this.items);

    this.todo = this.itemsObservable.pipe(
       map((item:Task[]) => item.filter((item:Task) => item.list === "Done")
     ));

    this.done = this.itemsObservable.pipe(
       map((item:Task[]) => item.filter((item:Task) => item.list === "To Do")
     ));    

    this.swimlanes.push({name:"To Do", array: this.todo});
    this.swimlanes.push({name:"Done", array: this.done});
  }

addItem(){
  this.items.push({name: "New Item", list: "To Do"});
  console.log(this.items.length);
}

moveItem(){

}

  drop(event: CdkDragDrop<string[]>) {

    if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      transferArrayItem(event.previousContainer.data,
                        event.container.data,
                        event.previousIndex,
                        event.currentIndex);
    }
  }
}

Here's a stackblitz of where I'm trying to use observables as the data source. You can see that it loads the lanes correctly, but when you attempt to move one of the items it dissapears. Any ideas?

In my actual app, I am receiving the values from an API request. So it automatically comes in as an Observable. Which I then break into a local base array and unsubscribe from the Observable. Then I create a new Observable from the local base array and make changes to the local base array in the hope that the changes will reflect in the drag drop. But this isn't currently working.

Happy to write my own move functionality, but without the lanes reflecting changes to the Observable data source, I can't get it to work.


Solution

  • I ran into the same problem. Apparently Angular DnD doesn't like observables in cdkDropListData. However there is a solution.

    The general idea is to not use observables as data sources for the droplist, but use observables.subscribe to populate the data source of the droplist.

    So instead of this:

    this.swimlanes.push({name: "To Do", array: this.todo})
    

    I did this:

    this.todo.subscribe(data => {
      console.log(data)
      this.swimlanes.push({name: "To Do", array: data})
    })
    

    You have to populate data to swimlanes from the subscribe method.

    I forked your stackblitz and made it work.

    stackblitz fork

    I hope this helps.

    Adding new item to the todo list:

    addItem(){
      this.baseTaskList.subscribe(items => {
        this.swimlanes[0]["array"]
          .push({"task": "New Task", "list":"To Do"})
          })
      }
    

    You'll add the new item to the droplist container in the subscribe of the http post assuming you'll also post the new item into some sort of database through some API. The response of post will be the ID of newly inserted item that you need to add to the item in the local container for future data manipulation where you need to know the row id (delete, update)