4

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);
    }
  }
}
StefaDesign
  • 929
  • 10
  • 19
  • 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 Nov 27 '19 at 02:08

4 Answers4

3

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.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • One additional fix probably needed for this is to check the event parameters for distance. Generally if the 'drop mouse coordinates' is not over or very close to the 'drop zone' then the user wants to abort. So you should probably check event and just ignore it if the distance is too great. There doesn't seem to be a value for this under `DragDropConfig` so I think you need to do it yourself. – Simon_Weaver Jun 30 '21 at 23:18
1

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.

Steve
  • 1,903
  • 5
  • 22
  • 30
  • Thank you Steve, this was great idea which actually made me adding some similar logic (using event.container.data.length === 0 instead of movementCount). One of problems I faced is that items could not be removed once selected so I had to fix logic to help user to select another image in case it was mistakenly selected. – StefaDesign Nov 27 '19 at 05:05
0

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;
}
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • WARNING: This answer (mine) may be misleading. I think the real reason it works is because I don't actually create any child items with `cdkDrag` on them. Turns out `[cdkDropListData]` is just a place you can put any data at all - it doesn't really represent the list of items that the `cdkDropList` uses to affect the behavior. – Simon_Weaver Jun 30 '21 at 23:15
0

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)
}
IAfanasov
  • 4,775
  • 3
  • 27
  • 42
Tony H
  • 1