Components: cdk-drop: allow drag-and-drop copy behavior

Created on 13 Sep 2018  路  49Comments  路  Source: angular/components

Bug, feature request, or proposal:

In some designs, you want to drag the same item repeatedly to multiple drop zones. In other words, the drag-and-drop system should support copy behavior as well as move behavior.

What is the expected behavior?

Developers should be able to choose whether a drag and drop acts as a move or a copy. This should be determined by the developer's implementation of the (dropped) handler and how they manipulate container data.

What is the current behavior?

<cdk-drop> seems to assume move behavior and has hidden state that prevents the developer from achieving copy behavior. In the (dropped) handler, if the developer leaves a container.data item in the previousContainer, it can be dragged again as expected. However, on the next drag-and-drop event.previousIndex = -1 and event.previousContainer is the wrong cdk-drop

To clarify:

  1. Drag item from cdk-drop source S to A
  2. In (dropped) handler, insert the dragged data item into A's data without removing it from S's data
  3. Drag same item from S to cdk-drop B
  4. Problem: (dropped) event has event.previousIndex = -1 and event.previousContainer is cdk-drop A instead of cdk-drop S.

What are the steps to reproduce?

Providing a StackBlitz reproduction is the best way to share your issue.

StackBlitz starter: https://goo.gl/wwnhMV

I can create a demo if needed.

What is the use-case or motivation for changing an existing behavior?

The current <cdk-drop> behavior is restrictive and doesn't allow for copy-drag designs.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Angular CLI: 6.2.1
Node: 8.11.4
OS: linux x64
Angular: 6.1.7
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                            Version
------------------------------------------------------------
@angular-devkit/architect          0.7.5
@angular-devkit/build-angular      0.7.5
@angular-devkit/build-optimizer    0.7.5
@angular-devkit/build-webpack      0.7.5
@angular-devkit/core               0.7.5
@angular-devkit/schematics         0.8.1
@angular/cdk                       6.4.7
@angular/cdk-experimental          6.4.7
@angular/cli                       6.2.1
@angular/flex-layout               6.0.0-beta.18
@angular/material                  6.4.7
@angular/material-moment-adapter   6.4.7
@ngtools/webpack                   6.1.5
@schematics/angular                0.8.1
@schematics/update                 0.8.1
rxjs                               6.2.2
typescript                         2.9.2
webpack                            4.9.2

Is there anything else we should know?

I worked-around this issue in my App by forcibly re-creating the <cdk-drop> within my template (toggle via *ngIf).

Also, in my app, I wanted to be able to drag items from the drop zones back to the source zone as a way of removing them. In this case, I remove the data item from the previousContainer.data but nothing in the destination container is changed. Again, I had issues with<cdk-drop> state and needed to apply my hack of re-generating the component.

needs discussion

Most helpful comment

Yes it copies the items correctly, but the UI is not fitting. In the drag event it pulls the item out of the sourceList and thats not the expected behavior. It will confuse the user.
Some update to leave the copy item to his sourceList would be very helpful

All 49 comments

Could my issue be related to yours? All i am trying is to "copy" new items to my dropzone.

13077

@kanidjar seems like the same issue

Look at the (dropped) event properties container, containerIndex, previousContainer, previousIndex and compare them with what you expect. In my case some of these would be wrong after dragging an item a second time.

Confirmed. Using your workaround fixes my issue :)

I am also facing the same issue,
I have 3 lists A,B and C where B and C will be my drop zones.
If items are moved from A then I cannot reuse the same item get pushed to other List. So, I guess drag and copy feature is required for to satisfy my requirement.
Any Suggestions / comments will be extremely helpful , Thank You.

When dropped inside B or C, you could temporarily call a method inside A that "resets" the array of items being displayed inside a timeout of 0 ms.
Something like:
this.items= []; setTimeout(() => { this.items= this.sourceItems.slice(); }, 0);

We used to have an issue where an item that was dragged into another container wasn't being reverted, however that has been resolved in 7.0.0-beta.1. What other issues are you running into?

Hey @crisbeto and @eden6 , Thanks for your response.
I will take an example so that the issue I am facing will be conveyed in simple terms.
I have all my employees list in List A, I want to drag drop employees who will be awarded bonuses in Quarter 1 to List B and employees who will be awarded bonuses in Quarter 2 to List C, but there are some employees who will receive in both Q1 & Q2. If I use drag and move feature, I can use the element in only 1 List among B or C . So, I require drag copy behavior instead of drag move so that I can clone Employee X in List B as well as List C. I don't want Employee X to be moved only to either of List B or List C making it not possible to use in both the Lists.

Once again thanks a lot for your responses. 馃憤

@SAkhil95 That's what I had understood. I don't know if a copy will be implemented, but in the meantime if you catch the droppedevent on containers B and C, you can reset the employees list displayed in A in order to get fresh items to drag again.
resetEmployeesList(){ this.employees= []; setTimeout(() => { this.employees= this.originalEmployeesList.slice(); }, 0); }
Hope this helps

@SAkhil95 are you also copying the object in your data model after the element has been dropped?

I have created a simple Stack blitz demo , I just want the items in List A to be copied to List B rather than move.
Please find below:
https://stackblitz.com/edit/angular-rxypv6
Thank You.

Here's what I was talking about:
https://stackblitz.com/edit/angular-w5ftfu

Yes @eden6 , this will certainly help me, Thanks a ton for the alternate solution mate.
Thanks @crisbeto, issue is now resolved with alternate solution provided by @eden6 .
Thank you both once again...

@eden6 you don't need to hack around it with timeouts and resetting the list. You just have to push the item to the new list without removing it from the old one: https://stackblitz.com/edit/angular-ymlmql. Closing the issue since this seems to work now.

Hello Guys, is there any easy way to delete items from div if drag dropped out of the div ??
@eden6 @crisbeto

@SAkhil95 I'm not sure if there is an event for dropping outside of a <cdk-drop>. I think the cdkDrag has a number of its own events. In my app, the user "removes" an item by dragging it back to the source <cdk-drop> and in that case I just remove it from the data array of the previousContainer being dragged from.

You'll need to determine what kind of <cdk-drop> it was dropped into, but that's up to you. I just look at the dropId.

Yes @arlowhite , I was thinking the same to push items back to main List , that will suffice the delete functionality. Thanks you very much.

@crisbeto @arlowhite
In each of your examples, the left list, although immutable, permutes its items while dragging an item from it and moving it over itself.
Is there a way to keep the items of the left list in place (which means (1) no permutation and (2) no removal of the dragged item when exiting its cdkDrop)?
Or should I open a new issue for that?

I鈥檓 not sure I completely get the use case. Are you asking whether it鈥檚 possible to disable sorting for a particular list, but still allow items from another one to be dropped into it?

Yes, I mean (1) disable sorting for a particular list.

But it would also be great, if I could tell a list, that its items don't disappear once they are entering another drop-area (2). In your example above, dragging an item from the left list removes it from this list as soon as it enters the right list. When it is dropped into the right list, it is also recovered in the left list.
I have such a list, which is only a provider of items and its items shall never leave the provider-area but only copies of those items. And the 'plop', when the item is recovered after drop doesn't feel good :)

That can definitely be set up, but the behavior around what happens when the item enters into the disabled list isn't very clear. I'm not sure whether the item would be added at the place where the user's cursor entered the new container or whether it would be appended to the list. Either way it might be frustrating for the user if they were trying to put it into a different position.

Okay, I understand, thanks for your response. Maybe that disable-sorting-feature should require an immutable list, where you can only drag items out of but not into it.
I just saw that there's already an issue for that: #13340

According to (2): Do you understand what I mean with that? Or shall I make it more clear? For our project, this is a crucial behavior.

If I understand it correctly, you want the item to both stay in the original list and show up in the new one?

Yes :)

@crisbeto think of it as an 'inventory' container that you only pull items from, for a vizio or lucidchart type app. You may want to sort the inventory but you would probably want to do that from a drag handle, and the drag item that we wish to clone may be inside your sortable container. There should probably be an option for limited or infinite cloning of the drag item as well.

I've been thinking of starting a PR for this, but I need some direction. Would you prefer to further configure the cdkDropList directive to be able to fit the requested use case or create a new directive with this functionality in mind?

If a new directive is preferred, I would also like to submit a sister directive, 'canvas', which will: fix dropped items where they are dropped in the container, account for scroll position, offer snap-to-grid, and scroll-on-hover-edge functionality.

If some of the shared functionality was abstracted into a base class, we could start building a library of directives to cover other use cases.

I'll submit a new issue for the canvas directive, I just wanted to get your thoughts first since this is your baby.

@nayfin The canvas idea is interesting.

IMO your first paragraph has ideas that the developer should just implement in their component. Tracking how often something has been dragged and sorting should be up to the developer. I'm not sure what you mean by sorting "from a drag handle".

Ideally, I think features like scroll-on-hover-edge would be some kind of generic Cdk thing. So you could use it on your canvas when dragging something. But other people could use it for other designs that need scroll-on-edge behavior. This would be more work, but add a lot more utility for developers.

@arlowhite I agree, unfortunately I don't currently see a way to toggle the sorting feature on the current cdkDropList. So, now when an item is dragged out the UI indicates that it was removed from the list even when there is no developer logic to do so. It then gets added back visually after dropped in the other list. @crisbeto's demo highlights this.

By sorting from a drag handle I mean that it's likely that you have a list of containers with drag items inside, the items you want to drag clone infinitely but you want to allow the user to sort the list of containers to suit their needs.

Heres a quick mock I made using lucidcharts. The up-down arrows represent the sort handles for the container, while the circles are the drag items. This would allow user to sort often used items to the top or wherever they find useful.
screen shot 2018-10-18 at 12 46 56 pm

I know much of this could be executed currently, I don't know how separate drag items will operate inside of a single cdkDropList though. I will get as close as I can with the library as is, and then update with a stackblitz. I'm riding the edge of getting off topic here and I don't want to pollute the thread. I'll start a fresh issue after I get some feedback.

EDIT: I got most of it working here. It was easier than I expected, kudos to the team. I just need a way to toggle off the sorting/transferring or toggle on a clone mode when I don't want to remove the drag item from the list ondragstart. I still have a lot of work to do making a canvas directive, I'll post progress to a new issue as soon as I have a POC.

@crisbeto can we please reopen. I don't think there has been a working solution proposed and I am in the middle of two PRs to resolve.

I'm also in favor of reopening this. When I want to copy an item, I expect the original item to stay in place, in other words I should be able to drag around the item and at the same time see the item at its original place. As a developer, this ideally is a just a property to set the mode.

Just my 2 cents:

I do think that the drag & drop API should be as flexible as possible. So even though it's more work, I like the fact that the developer needs to determine move/copy behavior and also whether to clone an object within the dropped handler. Here's mine as an illustration. I check the container ID prefix to determine if it was "copied" from source cdk-drop.

  onDrop(event: CdkDragDrop<ReportVisual[]>) {
    if (event.previousContainer === event.container) {
      // dragged within self
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    }
    else {
      const prevData = event.previousContainer.data;
      if (event.previousContainer.id.startsWith('visualSource')) {
        // dragged from Report visuals on left
        // need to clone, otherwise mutation will affect the source visual
        const visual = prevData[event.previousIndex].clone();
        // insert at index where dragged
        event.container.data.splice(event.currentIndex, 0, visual);
        // HACK which is the reason I created the GitHub issue
        // the parent resets the cdk-drops when this emits
        this.resetDragSources.emit();
      }
      else {
        // dragged from another chart lane
        transferArrayItem(prevData,
          event.container.data,
          event.previousIndex,
          event.currentIndex);
      }
    }
  }

Anyway, I think what people are asking for now is an aesthetic feature. I'd propose something like
[leaveGhost]=true
When you pickup these, the DOM elements would need to be cloned so that the original stays and there's something to drag. A CSS class would let you style the ghosted source however you want.

However, this actually sounds difficult to implement and I'm not sure it's a good design for Angular. Maybe a better and more powerful design would be the ability to set a optional template for the dragged item. This would let people transform the item during drag and also provide a system for duplicating the dragged item for this copy scenario. So the developer would need to set [leaveGhost]=true and also provide <ng-template draggingTemplate>

It really be cool to have this copy behavior, I think it actually solves part of my issue #13796

I have a PR ready to merge, that adds a helper function for making copies, similar to the transferArrayItem and moveItemInArray functions. I thought I knew how to implement drag clone functionality, but it's less straightforward than I initially thought. So, it's going to take me a little longer. If anyone else has dug into the source and has ideas please let me know.

@mmalerba I think this issue is actually two parts: copying an item to a target array in the logic, and adding functionality to tell UI to make a copy of drag item instead of dragging it out of the list then replacing it on drop. Here is a stackblitz highlighting what I described. The function added in #13743 helps with copying the item to the target array but not in addressing requested UI functionality.
If there is a way to configure this behavior in the current implementation, please advice here, I'll be happy to update docs with examples. Else, I think this issue should be reopened or I can create a new issue if you'd prefer that.

Hi @nayfin I've seen the pull request and the final result, it looks fine.
I have one question though: It seems the element being dragged out from the element is removed from the initial container when the drag start, and when the item is dropped it re appears in the initial container.
Is there a particular reason for that? can we change it, so he item won't leave the original container?

Thanks

@TKul6 this discussion probably belongs on the PR, not this issue, but I address those concerns in the comment above yours. #13743 doesn't touch the UI, it's just a helper function to facilitate copying from one array to another. I hope to address the UI in a separate PR as soon as I can find some free time.

I have working solution here. I had to do some strange nesting of drop list containers but it works and supports a limit to the copied items. IMPORTANT: I had to use a list inside a list to pull this off and, styling of the drag items is important. display: ablsolute and left and top are set to stack the drag items, this is what gives us the appearance of copying the drag item. It still needs a lot of refinement but I wanted to get it posted as soon as possible since so many people are wanting something like this.
This solution highlights how purpose driven this feature has been designed to be. I dug through the source to try and implement a change to the CdkDragListContainer that would allow this behavior but it is very tightly coupled to the Trello style use case and would have required a ton random if statements all over the place and would muddied the source code. I would like to see a base CdkDropContainer abstracted from the CdkDragListContainer that the CdkDragListContainer would extend to implement all the automatic ui behavior that we see.

Seems cool to me.
I'm not sure about the fact that when I drag the item back to the source container it seems like there are 2 items of the same source.

The display: absolute might also raise problems in a large scale application.

But other than that it looks great.

Thank you.

Thanks, I got copy working based on a few suggestions from here. Is there any way to 'disable' the automatic sorting of drop zone items when a dragged item is hovering over them?

So, I needed to create a toolbox with drag and drop functionality and recently started rewriting the old components we had, removing old and obsolete libraries which were not maintained any longer.

I actually found a way to have a (somewhat) proper copy behaviour, it was especially challenging due to the fact that we use placeholders to show the location of the new element. After going through the code, I found that, when using a placeholder, the old element just gets replaced in the HTML nodes, so, my solution basically reverses that. Demo can be found here:
https://stackblitz.com/edit/angular-material-drag-copy

What I've got now:

  • A toolbox from which users can drag any element to a dynamic form (using the custom addField method instead of just copying the element over, as there's some custom stuff going on that I left out)
  • Reordering of previously dragged elements inside the form
  • Toolbox items are not being sorted when dragging them (they all are in their own dropList elements)

Main things to take in account:

  • I store the old HTML node element inside the component, and replace it when the dragged is being moved, this is not ideal as it checks the condition every pixel moved...
  • I had to set a display: block !important CSS property on the element, which would be fine for simple element, but more complex ones might not like this

Only final thing I'm encountering now: sometimes there's a very short blinking effect of the placeholder ghost showing, I might be able to get that hidden with some css selectors.

I hope this helps anybody else needing copy behaviour

@Tommatheussen there is work in progress to enable custom drop-zones, these could selectively implement drop list methods and behaviors. See #14261. The issue is still open but looking at the current source, it seems like the required pieces are there. I plan attempting a custom drop-zone in a few weeks after finishing up some other stuff. I'll document if I'm successful and submit a PR to publish so that others can do the same. Then, hopefully we can expand the cdk to facilitate other common drag functionalities.

@Tommatheussen there is work in progress to enable custom drop-zones, these could selectively implement drop list methods and behaviors. See #14261. The issue is still open but looking at the current source, it seems like the required pieces are there. I plan attempting a custom drop-zone in a few weeks after finishing up some other stuff. I'll document if I'm successful and submit a PR to publish so that others can do the same. Then, hopefully we can expand the cdk to facilitate other common drag functionalities.

That's awesome news, I'll keep an eye out for more details on this. As we have to ship our software in a week or 3, I'll keep my current implementation as is and adapt when it's easier supported on the cdk, thanks for the heads up already :)

@Tommatheussen Thanks for the example, it really helped. There is an undesirable jitter that occurs when the element it removed and re-added to the DOM (between dragStart and dragMove). I haven't figured out how to get rid of that, it's ugly but not such a major issue.

However, the only additional functionality I need is to be able to drag an element outside of its container to remove it. Has anybody figured this out? Even if you drag the element way outside any drop area, cdkDropListDropped still fires with the original container. It would be great if event.container was null if an element was not dropped into any container, but that is not the case.

Does anybody have any ideas how to remove the element when it's dragged outside?

My suggestion for copy behavior would be to use "copyArrayItem" which is similar to "moveArrayItem". For more info see https://material.angular.io/cdk/drag-drop/api#functions

Any updates on drag and drop copy requirement.

Like @jayanlee say, this is a stackblitz example with copy https://stackblitz.com/edit/angular-xjex4y

Yes it copies the items correctly, but the UI is not fitting. In the drag event it pulls the item out of the sourceList and thats not the expected behavior. It will confuse the user.
Some update to leave the copy item to his sourceList would be very helpful

JQuery DnD have clone or copy mode, in CDK copy is working but drag preview is creating but the item is not showing in the actual list in UI until drag-drop operations completed.

You are right @DonsWayo . But i have done a small quick fix for this.

dragStart(event:CdkDragStart){
const tempTodo = event.source.dropContainer.data.slice();
const indexValue = tempTodo.indexOf(event.source.element.nativeElement.innerText);
const movableValue = tempTodo.splice(indexValue,1).join();
event.source.dropContainer.data.splice(indexValue, 0, movableValue);
}
drop(event: CdkDragDrop 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);
}
}

Copy these functions to you ts code. But there is another catch here, the reason why copy functionality is like that because the element that is dragged might not know whether its going to be put in another droplist or same droplist. This quick fix here works only if you put it in another droplist. If you try to put it or swap inside same droplist it creates a redundant name of the draggable element.

Please provide similar behavior like https://jsfiddle.net/drzaus/mP8kY/ in cdk dnd

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jelbourn picture jelbourn  路  3Comments

3mp3ri0r picture 3mp3ri0r  路  3Comments

constantinlucian picture constantinlucian  路  3Comments

MurhafSousli picture MurhafSousli  路  3Comments

vanor89 picture vanor89  路  3Comments