4
votes

When using @angular/cdk/drag-drop module (Angular Material Drag and Drop)... Is there any way to limit drop container so to accept only one value instead of multiple values? I am trying to create form where user can drag image and drop into field which should have only one item. I am using standard example code for implementation from Drag and Drop | Angular Material but cannot find solution where number of dropped items can be limited, and second cannot find solution to keep Drag list the same (dragged item will stay in drag container) so you copy instead of move item to Drop container. Is there any solution or someone who can help with sample code?

HTML:

    <div class="example-container">
  <h2>To do</h2>

  <div
    cdkDropList
    #todoList="cdkDropList"
    [cdkDropListData]="todo"
    [cdkDropListConnectedTo]="[doneList]"
    class="example-list"
    (cdkDropListDropped)="drop($event)">
    <div class="example-box" *ngFor="let item of todo" cdkDrag>{{item}}</div>
  </div>
</div>

<div class="example-container">
  <h2>Done</h2>

  <div
    cdkDropList
    #doneList="cdkDropList"
    [cdkDropListData]="done"
    [cdkDropListConnectedTo]="[todoList]"
    class="example-list"
    (cdkDropListDropped)="drop($event)">
    <div class="example-box" *ngFor="let item of done" cdkDrag>{{item}}</div>
  </div>
</div>

TS:

import {Component} from '@angular/core';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';

/**
 * @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 {
  todo = [
    'Get to work',
    'Pick up groceries',
    'Go home',
    'Fall asleep'
  ];

  done = [
    'Get up',
    'Brush teeth',
    'Take a shower',
    'Check e-mail',
    'Walk dog'
  ];

  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);
    }
  }
}
4
You'd need to write extra code to achieve both of those things but it is achievable. I recently had to update CdkDragDown for some special stuff (neither of your requirements unfortunately). I'll have a play tonight if no one has answered the question before then.Steve

4 Answers

1
votes

OK, this should work:

movementCount: number = 0;

drop(event: CdkDragDrop<string[]>) {
   if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else if(this.movementCount === 0){
       this.movementCount++;  // block next object from being moved
       // copy obj being moved
       var movingObj = event.previousContainer.data[event.previousIndex];

       transferArrayItem(event.previousContainer.data,
                    event.container.data,
                    event.previousIndex,
                    event.currentIndex);

       // transfer complete so copy object back
       event.previousContainer.data.push(movingObj);    
  }
}

I used a counter to determine if the move was allowed but a boolean would work just as well (better). You just need to add extra code so when the image is removed/deleted from the div it was carried into, the counter is returned to zero/false so another image can be dragged if needed.

Hope that helps.

1
votes

Here's a working demo of my version that allows for overwriting.

Make sure your screen is wide enough you can see both columns.


You should use cdkDropListEnterPredicate

Just return false from this if your destination 'slot' is already filled.

Note that your handler is called from a context where this is not your component. So you need to use a lambda function like this in your component.

destinationNotEmptyPredicate = () => 
{
   return this.destinationArray.length == 0;
};

Make sure the destination list has a height such that you can actually drop something in it.

However if you need to allow overwriting of the existing single item in your 'fake list' (and you probably do) then it's a little trickier. You wouldn't want to use this predicate (because it would stop you from dropping anything unless you first deleted the existing item).

So in that case you need to do a css tweak to hide the item that already exists (when you hover over it).

#even is the destination list.

#even.cdk-drop-list-dragging .cdk-drag:not(.cdk-drag-placeholder) {
   display: none;
}

Also set [cdkDropListSortingDisabled]="false" to avoid a weird animation glitch.

When you 'drop' the item your destination list should be made to just contain one item:

drop(event: CdkDragDrop<number[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    } else {
      this.even = [event.item.data];
    }
  }

Notice how I'm still using the cdkDropListEnterPredicate here to allow only even numbers to be dropped.

0
votes

Another solution is just set the [cdkDropListData]="[]" as a hardcoded empty list. So it's a list from the perspective of the cdk, but not really for your app.

Then you just display in that spot whatever you want (if the item is set), and handle the cdkDragDrop event to update it.

    <div class="slot"        
         cdkDropList
         [cdkDropListData]="[]"
         (cdkDropListDropped)="drop($event)">

       <!-- display your item here with *ngIf -->

    </div>


.slot
{
   min-height: 50px;
}
0
votes

Not sure if it's still useful, but I've used 2 methods to limit the drop item to 1 only and to cancel the previous dropped item if another one is dropped:

  1. disable sorting in HTML

    cdkDropListSortingDisabled

  2. in .ts limit the total amount of items to 1, and remove the one at index 1 (the one already inside), since the sorting disable will add the next at index 0

Add the following function on your onDrop method:

for (let  i=0 ; i<2 ; i++){
   this.items.splice( 1 ,1)
}