dnd-kit icon indicating copy to clipboard operation
dnd-kit copied to clipboard

Placeholders

Open 5ebastianMeier opened this issue 2 years ago • 3 comments

@clauderic first of all: Thanks for this fantastic lib! I've invested so much time to adapt and marry react-dnd and react-beautiful-dnd before realizing that dnd-kit may be able to make my life a LOT easier.

Motivation

When I first started using dnd-kit I didn't even notice that placeholders were not supported that prominently, because in the app I maintain most drop zones only show a full overlay over the dropzone when you drag an element over them that can be dropped.

In a common sortable scenario where you only move an existing entry around placeholders are also not a problem. But when I tried to implement a dynamic list where entries can be added and sorted in the same step via drag-n-drop I started to realize that this can be very complicated.

Especially when you use custom appearances for one or more of the elements participating in the operation (e.g. custom drag overlay that represents an abstract type, but becomes a specialized type once you move it onto a droppable). Furthermore I felt a lack of control to independently style and customize certain behaviors.

Another thing that I noticed is that drop animations are limited in terms of animating to a drop target, because a drag mostly seems to be treated as a "move" operation. In case of a "copy" operation the placeholder can also serve as a target to animate the drop to.

Demo

I've added two Stories that demonstrate the behaviors this PR enables.

These videos give you a glimpse of the functionality - feel free to check out the branch and try it yourself.

Placeholder for a regular Droppable

https://user-images.githubusercontent.com/16539774/168794615-110769a7-f9ce-4d75-8dff-511f32f713e5.mov

Dynamic Placeholder for a Sortable

https://user-images.githubusercontent.com/16539774/168794940-8d7a18f2-4c6a-4ac9-993e-b9ffe08d1fcc.mov

Dynamic Placeholder with a custom height DragOverlay

https://user-images.githubusercontent.com/16539774/168795026-d9ac4a27-89a5-49eb-8498-5870f267fcd6.mov

Dynamic Placeholder that tracks the custom height of the DragOverlay

https://user-images.githubusercontent.com/16539774/168795104-df702731-ec2b-4256-95b6-eb98bedd5957.mov

Implementation

This is just my naive first try to implement the behavior I was missing. I'm sure there are edge cases that I might have missed and the API can certainly be improved as well.

I'd rather use this as a starting point for the discussion. There are some dirty hacks included that I didn't put too much effort into yet (e.g. string-replace to parse an id when I didn't have a good idea how to achieve it in a clean fashion).

Considerations

I tried to make it more comfortable for developers by adding the active item to the SortableContext dynamically without forcing it to be added to the list of items externally. This should result in a cleaner API as there can be a lot of moving parts to keep track of.

The dynamic placeholder is only added to a SortableContext if the drag didn't start inside it. This allows for regular sorting in the source container and copy-style sorting in target containers.

Naive API

Droppable Placeholder

const placeholderId = useUniqueId('Placeholder');

<DndContext>
  <Droppable
    key={id}
    id={id}
    placeholderId={placeholderId}
  >
    {hasItem ? item : null}
    {isPlaceholderActive && (
      <PlaceholderItem
        placeholderId={placeholderId}
        placeholderContainerId={id}
      />
    )}
  </Droppable>
</DndContext>

Sortable Placeholder

const placeholderId = useUniqueId('Placeholder');
const containerId = 'test';
const containerPlaceholderId = `${containerId}-${placeholderId}`;
const sortableId = `sortable-${containerId}`;

<DroppableContainer
  key={containerId}
  id={containerId}
  items={items}
  placeholderId={containerPlaceholderId}
>
  <SortableContext
    id={sortableId}
    items={items}
  >
    {items.map((value, index) => {
      return (
        <SortableItem
          placeholderId={containerPlaceholderId}
          placeholderContainerId={sortableId}
          key={value}
          id={value}
          index={index}
          containerId={containerId}
        />
      );
    })}
    {isPlaceholderActive && (
      <PlaceholderItem
        id={containerPlaceholderId}
        placeholderId={containerPlaceholderId}
        placeholderContainerId={sortableId}
        key={containerPlaceholderId}
        index={items.length}
        containerId={containerId}
      />
    )}
  </SortableContext>
</DroppableContainer>

function PlaceholderItem(props: SortableItemProps) {
  return <SortableItem placeholder {...props} />;
}

const {
  active,
  setNodeRef,
  listeners,
  isDragging,
  isSorting,
  over,
  overIndex,
  transform,
  transition,
} = useSortable({
  id,
  placeholder,
  placeholderId,
  placeholderContainerId,
});

5ebastianMeier avatar May 17 '22 11:05 5ebastianMeier

⚠️ No Changeset found

Latest commit: 66141896d62ca27201b5d3f170e365ba4b2c4781

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

changeset-bot[bot] avatar May 17 '22 11:05 changeset-bot[bot]

Hey @5ebastianMeier, thanks for opening a PR and for providing detailed motivation and rationales for this change ❤️

I'm a bit reluctant to introduce this type of complexity into the built-in concepts of @dnd-kit.

There are already a lot of concepts that consumers need to understand, and I'm not sure whether this is unlocking net new functionality that can't otherwise be achieved with the existing concepts that @dnd-kit provides out of the box.

None of the existing @dnd-kit primitives are opinionated about rendering; introducing the concept of placeholders into the built-in concepts feels like it would blur the lines between the consumer being fully in control of rendering and state management.

Having that said, I totally see the value in making this type of use-case easier for consumers; I just have a different approach in mind for how to achieve that goal.

One of the things that has been on the roadmap for quite some time but that I have not had time to prioritize yet is to create one or multiple @dnd-kit packages that would export components built on top of the core concepts of @dnd-kit and that could expose concepts such as placeholders.

import {MultipleContainers} from '@dnd-kit/sortable-components';

function App() {
  const [items, setItems] = useState({A: ['A1', 'A2', 'A3'], B: ['B1', 'B2', 'B3']});

  return (
    <MultipleContainers
      items={items}
      onItemsChange={setItems}
      renderContainer={...}
      renderItem={...}
      renderPlaceholder={...}
   />
  )
}

I'm very appreciative of the time and effort you've put into this PR, it looks very polished. In the future I would recommend starting a discussion beforehand just to align on whether or not the functionality you have in mind fits within the goals of the library.

clauderic avatar May 18 '22 21:05 clauderic

Thanks for the insights @clauderic

I totally agree about the API I used for the demo not being suitable for the existing components.

The code I used for the demo was just meant to show the behavior. I want to built a lot of cool stuff on the basis of @dnd-kit so I needed to better understand the internals, get a feel for it and find out if and how it is able to cover my use cases. A good thing with a working prototype like this is that I can try to refactor it while trying not to break the demo.

I would also prefer to keep the placeholders in a separate package, but I was a bit lost on how the API should ideally look like. The sample code you provided would definitely hit a very common use case so I'm down to offering my help to cover it as a part of the topic as a whole as well. If that scenario can be solved my original one should be no problem as well.

No idea if there's any big obstacles ahead, but at first glance I should be able to refactor the demo code into a component like that. If you don't mind I'll just give it a try again and see how far I get.

If there's additional aspects that are important to you or things that you'd like considered don't hesitate to mention them as well. I know it's a lot of work maintaining a lib like this so my goal is to put some work into this so you don't have to (and ideally ship this feature a bit earlier than would otherwise be possible).

5ebastianMeier avatar May 18 '22 23:05 5ebastianMeier