haltu / muuri

Infinite responsive, sortable, filterable and draggable layouts
https://muuri.dev
MIT License
10.77k stars 644 forks source link

A Drag and Drop Tip for Muuri and Databases #489

Open ibrychgo opened 3 years ago

ibrychgo commented 3 years ago

As we built out our kanban and virtual corkboards with muuri, we ran across a lot of the same challenges I see mentioned in this forum.

I'm posting this tip to help those who are new to Muuri and might be expecting similar hooks that they've used in other libraries.

Simply put, when dragging and dropping, depending on your application, you likely need good information about where you are dragging from, what you are dragging, what you are dragging over, and finally, where an item was dropped from a DATA perspective. While Muuri makes it easy to manipulate items, syncing that up with your data models can be a challenge.

This is how we solved it for our multi-grid application.

1) HTML Every item gets a data-id attribute that matches their unique database ID and a data-order attribute containing the items current sort position. Even if the item is a nested grid. 2) We created a global object for each type of grid that has dragging. In our case it was itemDrag for task cards, and columnDrag for draggable lists. Their structure is identical so I'm only sharing itemDrag:

itemDrag = {
     item: undefined,
     itemID: undefined,
     itemData: undefined,
     sourceGrid: undefined,
     sourceGridID: undefined,
     sourceIndex: undefined,
     sourceScrollPosition: undefined,
     targetGrid: undefined,
     targetgridID: undefined,
     targetScrollPosition: undefined
}

3) In the our grid constructors, we leverage the following events to populate this object during the drag operation:

.on('dragStart', function(item, event) {
          // define some of our itemDrag properties
          itemDrag.item = item;
          itemDrag.itemID = item.getElement().getAttribute('data-id');          
          itemDrag.sourceGrid = item.getGrid();
          itemDrag.sourceGridID = itemDrag.sourceGrid.getElement().getAttribute('data-id');
          itemDrag.sourceIndex = itemDrag.sourceGrid.getItems().indexOf(item);
          itemDrag.sourceScroll = $(itemDrag.sourceGrid.getElement()).closest('.board-column-content-wrapper').scrollTop();
          itemDrag.itemData = getItemData(itemDrag.itemID)
.on('receive', function(data) {
          // add targetGrid information to itemDrag
          itemDrag.targetGrid = data.toGrid;
          itemDrag.targetGridID = data.toGrid._element.getAttribute('data-id');
          itemDrag.targetScroll = $(itemDrag.targetGrid.getElement()).closest('.board-column-content-wrapper').scrollTop();
        })

Note: on.receive will get called multiple times as users drag objects over other grids, altering the target information multiple times. When they finally drop the item, this data will contain the final destination information.

 .on('dragReleaseEnd', function(item) {
            if (itemDrag.targetGrid) {
              itemDrag.targetIndex = itemDrag.targetGrid.getItems().indexOf(item);
            }
            // Process the drag and drop action
            return updateItemDrop(itemDrag)
              .then(function() {
                  // reset itemDrag
                  resetDragData();
              })
          }
        });

So at the end of the drop, we have everything we need to know about what happened, and where. We don't have to go looking for the sourceGrid object if we need to adjust our item's previous siblings, likewise for the targetGrid. This lets us properly update our model/database and do almost any other logic we need without looping through grids and items to see what changed.

Another benefit is that as a user goes through the various drag and drop events, you'll know enough about the item and where it's at to add additional logic in the event handlers to handle exceptions and edge cases even before the item is dropped.

4) The updateItemDrop function simply makes the proper changes to the database (removing items from lists, adding them to new ones, re-sorting items in a grid, etc.)

5) Watchers on our database trigger onChange, onAdded, onRemove events which we use to respond to any changes made with updateItemDrop (or any other function) to update items within Muuri. If an item is changed, we generate a fresh copy, remove the old one and add the new one to muuri. Simple as that. No flickering. No weird layouts. Just smooth and solid drag and drop performance.

I'm sorry I haven't provided a codePen but it's just not possible given how our application is set up.

Quixomatic commented 3 years ago

I just spent the last day trying to decide how to connect this front end functionality to the actual data model. Thank you for posting this, its nice to see how someone else solved a similar problem.

ibrychgo commented 3 years ago

You’re most welcome. :)