can a nested <DndContext /> detect draggable/droppable elements that's coming from another <DndContext /> (parent dndContext for example)?
Long story short, i have 3 module federation apps that are combined together, 1 main app (father), 1 sidebar app, 1 content. each app has its own DndContext for sortable lists. I'm trying to drag elements from the sidebar(_app) to the content(_app), but each one has its own <DndContext /> so one does not detect the other. so i wrapper the 2 apps with a 1 <DndContext /> and it works but its more complicated now because each one has its own actions and its own settings.
the question is, can <DndContext /> detect draggable/droppable elements that's coming from another <DndContext />?
Thank you
I think it's not possible based on what I read. You need to use the same context. It's not practical but achievable. Here is how I implemented mine https://app.simplyfitness.com/workout-builder/new
I think it's not possible based on what I read. You need to use the same context. It's not practical but achievable. Here is how I implemented mine https://app.simplyfitness.com/workout-builder/new
is there a repo or a code that I can take a look at, I don't want your private repo but maybe something similar
I will publish the code on sandbox.
I had the same problem. I adopted a workaround using a placeholder component. This is my current situation:
- a page context;
- a sortable tree on the left side (
SortableTreecomponent), with categories that can be re-organised, that has it's own context; - a list of items on the right side, rendered in any way, which is directly correlated to the page context.
My goal was to have the element dropped on the categories on the left side. With the current setup, it was not possible due to double context, so I added the following modifications:
- the
TreeItemcomponent accepts aTreeItemWrapper, that by default it's just aFragment(no-op); - added a new component
SortableTreePlaceholderthat uses the same logic and styles of the "normal" one, but without DnD, so the UI result is exactly the same, and I pass a component asTreeItemWrapperthat is enriched withuseDroppable; - use
{active} = useDndContext()and render conditionally the components based on the value: ifactiveistrue, useSortableTreePlaceholderso the items from the right side can be dropped inside, otherwise renderSortableTreethat has it's own context.
Dummy code:
// SortableTree like in the examples
function SortableTree({ items, ...other }) {
const flattenedItems = flattenTree(items);
return (
<DndContext
onDragEnd={(props) => handleDragEnd({ ...props, onDragEnd })}
{...otherProps}
>
<SortableContext items={sortedIds} strategy={verticalListSortingStrategy}>
{flattenedItems.map(({ id }) => <SortableTreeItem key={id} {...otherSortableProps} /> )}
{createPortal(
<DragOverlay dropAnimation={dropAnimationConfig}>
{activeId && activeItem ? <SortableTreeItem id={activeId} /> : null}
</DragOverlay>,
document.body
)}
</SortableContext>
</DndContext>
);
}
// SortableTreeItem, very similar to the one in the examples
function SortableTreeItem(props) {
return <TreeItem {...props} />
}
// TreeItem is similar to the examples, but with the addition of the TreeItemWrapper prop
export const TreeItem = forwardRef<HTMLDivElement, Props>(
({id, wrapperRef, TreeItemWrapper, className, value, ...props}, ref) => {
const Wrapper = TreeItemWrapper ?? DefaultTreeItemWrapper;
return (
<li
className={classNames(styles.Wrapper, className)}
ref={wrapperRef}
style={{'--spacing': `${indentationWidth * depth}px`, }}
{...props}
>
<Wrapper id={id}>
<div className={cx(styles.TreeItem, 'draggable-item')} ref={ref} style={style}>
<Icon name="handler" />
<span className={styles.Text}>{value}</span>
</div>
</Wrapper>
</li>
);
}
);
function DefaultTreeItemWrapper({ id, children }: TreeItemWrapperProps) {
return <Fragment>{children}</Fragment>;
}
Then, the *Placeholder one:
export function SortableTreePlaceholder({ treeItems, TreeItemWrapper }) {
const [items, setItems] = useState(() => treeItems);
const flattenedItems = flattenTree(items);
return (
<>
{flattenedItems.map(({ id, depth }) => (
<TreeItemPlaceholder key={id} id={id} TreeItemWrapper={TreeItemWrapper} />
))}
</>
);
}
const TreeItemPlaceholder = forwardRef<HTMLDivElement, Props>(
({id, wrapperRef, TreeItemWrapper, className, value, ...props}, ref) => {
const Wrapper = TreeItemWrapper ?? DefaultTreeItemWrapper;
return (
<li
className={classNames(styles.Wrapper, className)}
ref={wrapperRef}
style={{'--spacing': `${indentationWidth * depth}px`, }}
{...props}
>
<Wrapper id={id}>
<div className={cx(styles.TreeItem, 'draggable-item')} ref={ref} style={style}>
{/* NO handler here */
<span className={styles.Text}>{value}</span>
</div>
</Wrapper>
</li>
);
}
);
And finally the one that calls them:
function Page() {
return <DndContext sensors={sensors} onDragEnd={onDragEnd}>
<Dashboard />
</DndContext>
}
function Dashboard() {
const {active} = useDndContext();
return <div className="dashboard-container">
<div className="side-panel">
{active
? <SortableTreePlaceholder TreeItemWrapper={TreeItemWrapper} items={items} />
: <SortableTree items={items} /> :
</div>
<div className="main-content">
<SomeComponentWithUseDraggable />
</div>
</div>
}
function TreeItemWrapper({ id, children }: TreeItemWrapperProps) {
const onDrop = async (draggableData, droppableData) => {
console.log('DROPPED!',);
};
const { isOver: isDroppableOver, setNodeRef } = useDroppable({
id: sourceDroppableId,
data: {
id: sourceDroppableId,
droppableData: {} // data that I am forwarding around for reference, with IDs etc
},
});
return (
<div
className={classNames(styles['item'], {
[styles['item--selected']]: selectedSectionId === id,
[styles['item--over']]: isDroppableOver,
})}
ref={setNodeRef}
>
{children}
</div>
);
}
Please note all of these are dummy examples, made just for logical understanding and not to be copy-pasted as my case can be very different from others.
I had the same problem. I adopted a workaround using a placeholder component. This is my current situation:
- a page context;
- a sortable tree on the left side (
SortableTreecomponent), with categories that can be re-organised, that has it's own context;- a list of items on the right side, rendered in any way, which is directly correlated to the page context.
My goal was to have the element dropped on the categories on the left side. With the current setup, it was not possible due to double context, so I added the following modifications:
- the
TreeItemcomponent accepts aTreeItemWrapper, that by default it's just aFragment(no-op);- added a new component
SortableTreePlaceholderthat uses the same logic and styles of the "normal" one, but without DnD, so the UI result is exactly the same, and I pass a component asTreeItemWrapperthat is enriched withuseDroppable;- use
{active} = useDndContext()and render conditionally the components based on the value: ifactiveistrue, useSortableTreePlaceholderso the items from the right side can be dropped inside, otherwise renderSortableTreethat has it's own context.Dummy code:
// SortableTree like in the examples function SortableTree({ items, ...other }) { const flattenedItems = flattenTree(items); return ( <DndContext onDragEnd={(props) => handleDragEnd({ ...props, onDragEnd })} {...otherProps} > <SortableContext items={sortedIds} strategy={verticalListSortingStrategy}> {flattenedItems.map(({ id }) => <SortableTreeItem key={id} {...otherSortableProps} /> )} {createPortal( <DragOverlay dropAnimation={dropAnimationConfig}> {activeId && activeItem ? <SortableTreeItem id={activeId} /> : null} </DragOverlay>, document.body )} </SortableContext> </DndContext> ); } // SortableTreeItem, very similar to the one in the examples function SortableTreeItem(props) { return <TreeItem {...props} /> } // TreeItem is similar to the examples, but with the addition of the TreeItemWrapper prop export const TreeItem = forwardRef<HTMLDivElement, Props>( ({id, wrapperRef, TreeItemWrapper, className, value, ...props}, ref) => { const Wrapper = TreeItemWrapper ?? DefaultTreeItemWrapper; return ( <li className={classNames(styles.Wrapper, className)} ref={wrapperRef} style={{'--spacing': `${indentationWidth * depth}px`, }} {...props} > <Wrapper id={id}> <div className={cx(styles.TreeItem, 'draggable-item')} ref={ref} style={style}> <Icon name="handler" /> <span className={styles.Text}>{value}</span> </div> </Wrapper> </li> ); } ); function DefaultTreeItemWrapper({ id, children }: TreeItemWrapperProps) { return <Fragment>{children}</Fragment>; }Then, the *Placeholder one:
export function SortableTreePlaceholder({ treeItems, TreeItemWrapper }) { const [items, setItems] = useState(() => treeItems); const flattenedItems = flattenTree(items); return ( <> {flattenedItems.map(({ id, depth }) => ( <TreeItemPlaceholder key={id} id={id} TreeItemWrapper={TreeItemWrapper} /> ))} </> ); } const TreeItemPlaceholder = forwardRef<HTMLDivElement, Props>( ({id, wrapperRef, TreeItemWrapper, className, value, ...props}, ref) => { const Wrapper = TreeItemWrapper ?? DefaultTreeItemWrapper; return ( <li className={classNames(styles.Wrapper, className)} ref={wrapperRef} style={{'--spacing': `${indentationWidth * depth}px`, }} {...props} > <Wrapper id={id}> <div className={cx(styles.TreeItem, 'draggable-item')} ref={ref} style={style}> {/* NO handler here */ <span className={styles.Text}>{value}</span> </div> </Wrapper> </li> ); } );And finally the one that calls them:
function Page() { return <DndContext sensors={sensors} onDragEnd={onDragEnd}> <Dashboard /> </DndContext> } function Dashboard() { const {active} = useDndContext(); return <div className="dashboard-container"> <div className="side-panel"> {active ? <SortableTreePlaceholder TreeItemWrapper={TreeItemWrapper} items={items} /> : <SortableTree items={items} /> : </div> <div className="main-content"> <SomeComponentWithUseDraggable /> </div> </div> } function TreeItemWrapper({ id, children }: TreeItemWrapperProps) { const onDrop = async (draggableData, droppableData) => { console.log('DROPPED!',); }; const { isOver: isDroppableOver, setNodeRef } = useDroppable({ id: sourceDroppableId, data: { id: sourceDroppableId, droppableData: {} // data that I am forwarding around for reference, with IDs etc }, }); return ( <div className={classNames(styles['item'], { [styles['item--selected']]: selectedSectionId === id, [styles['item--over']]: isDroppableOver, })} ref={setNodeRef} > {children} </div> ); }Please note all of these are dummy examples, made just for logical understanding and not to be copy-pasted as my case can be very different from others.
very very interesting solution! i will try to implement it on my end and see if it fits. Thanks !!
I will publish the code on sandbox.
Hi, could you please share the code? Thank you
@vittoriozamboni Thank you for sharing your implementation. I have reached a similar solution but I'm observing some glitches due to conditional rendering. I was wondering if you experienced the same and whether/how you solved it, specially here:
<div className="side-panel"
{active
? <SortableTreePlaceholder TreeItemWrapper={TreeItemWrapper} items={items} />
: <SortableTree items={items} /> }
</div>