Closed deje1011 closed 4 months ago
Hmm, I'm curious why I'm not seeing this breaking today in my examples / usage.
Can you share a reproducible example to illustrate the issue more clearly?
I think the examples just never use the props
to build the id
or data
of the draggable/droppable.
I adjusted the "Basic Drag & Drop" example and pass a selected animal to the draggable:
In onDragEnd
, draggable.id
will always be "Dog", no matter which animal was selected.
Using createReactiveDraggable
instead of createDraggable
fixes that.
Hope this helps, let me know if I should explain it in more detail :)
import {
DragDropProvider,
DragDropSensors,
type DragEventHandler,
createDraggable,
createDroppable,
} from '@thisbeyond/solid-dnd';
import {createSignal, Show} from 'solid-js';
const [getAnimals] = createSignal(['Dog', 'Cat', 'Rat', 'Elephant']);
const [getSelectedAnimal, setSelectedAnimal] = createSignal(getAnimals()[0]);
const selectNextAnimal = () => {
const animals = getAnimals();
const index = animals.indexOf(getSelectedAnimal());
setSelectedAnimal(animals[(index + 1) % animals.length]);
};
const Draggable = props => {
const draggable = createDraggable(props.animal);
// const draggable = createReactiveDraggable(() => props.animal);
return (
<div use:draggable class='draggable'>
{props.animal}
</div>
);
};
const Droppable = props => {
const droppable = createDroppable(1);
return (
<div use:droppable class='droppable' classList={{'!droppable-accept': droppable.isActiveDroppable}}>
Droppable.
{props.children}
</div>
);
};
export const DragAndDropExample = () => {
const [where, setWhere] = createSignal('outside');
const [droppedAnimal, setDroppedAnimal] = createSignal(null);
const onDragEnd: DragEventHandler = ({droppable, draggable}) => {
setDroppedAnimal(draggable.id);
if (droppable) {
setWhere('inside');
} else {
setWhere('outside');
}
};
return (
<DragDropProvider onDragEnd={onDragEnd}>
<button onClick={selectNextAnimal}>Next</button>
<DragDropSensors />
<div class='min-h-15'>
<Show when={where() === 'outside'}>
<Draggable animal={getSelectedAnimal()} />
</Show>
</div>
<Droppable>
<Show when={where() === 'inside'}>
<Draggable animal={getSelectedAnimal()} />
</Show>
</Droppable>
<Show when={droppedAnimal()}>
<div>Dropped: {droppedAnimal()}</div>
</Show>
</DragDropProvider>
);
};
Can confirm, I ran into the same issue with a very similar simple example:
const draggable = createDraggable(id(), { itemName: item.name, itemType: item.itemType });
The itemName
and itemType
are stale and always have the first value. If they change in the component, the old value will still be passed because it's not reactive. I checked the examples and the only ones where the data passed was being modified in any way were the ones using createSortable
so not sure if the issue is just not present there.
This is my whole component if you're curious:
type ItemProps = {
id: string;
};
export const Item: Component<ItemProps> = (props) => {
const { treeStore, toggleCollapsed } = useTreeContext();
const item = useItemContext();
const id = () => props.id;
const draggable = createDraggable(id(), { itemName: item.name, itemType: item.itemType });
const droppable = createDroppable(id());
let nameInputElement!: HTMLDivElement;
let containerElement!: HTMLDivElement;
const childrenIds = createMemo(() => {
return childrenToSortedByTypeName(treeStore, props.id);
});
createEffect(() => createShortcuts(containerElement, nameInputElement));
return (
<div data-tree-item-container="container">
<div ref={draggable.ref}>
<div
use:droppable
data-tree-item={props.id}
data-tree-item-level={item.level}
class={`${droppable.isActiveDroppable ? 'bg-secondary bg-opacity-20' : ''} relative my-1 flex w-fit items-center text-nowrap rounded-md border-[1px] border-dashed border-secondary pl-2 pr-4 focus:bg-base-200 focus:bg-opacity-30 focus:outline focus:outline-2 focus:outline-secondary`}
tabIndex={0}
role="treeitem"
// style={transformStyle(draggable.transform)}
ref={containerElement}
>
<Show when={!isRootId(props.id)}>
<div {...draggable.dragActivators} class="absolute -left-6 hover:cursor-grab">
<DotsSixVertical />
</div>
</Show>
<Show when={isDirectory(item)}>
<button onClick={() => toggleCollapsed(props.id)}>
{!item.isCollapsed ? <CaretDown /> : <CaretRight />}
</button>
</Show>
<div class="mx-1">{item.itemType === 'directory' ? <Directory /> : <File />}</div>
<NameInput focusElement={containerElement} ref={nameInputElement} />
</div>
</div>
<Show when={!item.isCollapsed}>
<div class="border-l-2 pl-8">
<For each={childrenIds()}>
{(childId) => (
<ItemProvider id={childId}>
<Item id={childId} />
</ItemProvider>
)}
</For>
</div>
</Show>
</div>
);
};
🤔 I've been thinking about this a bit more.
The current behaviour is in line with how Solid is designed - calls in a non-tracking context are not reactive by default. The sortable example works because the list itself is reactive and the components that have changed are recreated in that tracking context (and so createDraggable
is called again for the new nodes).
Each draggable id
should be unique to the instance of the draggable itself (the call to createDraggable
), not the data it represents. So it doesn't make sense for this to change reactively without recalling createDraggable
.
However, note that the data
argument itself is just a pass through (and isn't used internally by solid-dnd at all). If you want to make it maintain reactivity then you can pass data that is reactive itself and access it in tracking context (e.g. the JSX). For example:
const draggable = createDraggable(id(), {
get itemName(): { return item.name },
get itemType(): { return item.itemType }
});
// Later in the JSX
<div>
{draggable.data.itemName} // Now reactive!
</div>
If item
is already a reactive object then you could pass it directly instead without wrapping: createDraggable(id(), item)
Hope that helps and going to close this now as wontfix
Thank you for the explanation @martinpengellyphillips ! I've applied your suggestion and it does indeed work! Thanks 😊
Hi,
First of all, thanks a lot for building this library!
While using it to implement drag and drop in my current project, I ran into an issue regarding reactivity. Consider the following example:
When
props.item
changes, the draggableid
anddata
will be outdated, leading to the wrong item being modified on drop.One way to fix this is to re-render the component when the item.id changes:
This way, the
id
will be in sync but thedata
might still be outdated.Another way to fix this would be to pass reactive getters to
createDraggable
/createDroppable
instead of values:But of course this would be a breaking change of the API.
What do you think? Or maybe there is another way to solve this?