clauderic / dnd-kit

The modern, lightweight, performant, accessible and extensible drag & drop toolkit for React.
http://dndkit.com
MIT License
12.86k stars 640 forks source link

can a nested <DndContext /> detect draggable/droppable elements that's coming from another <DndContext /> (parent dndContext for example)? #1173

Open alaabashiyi opened 1 year ago

alaabashiyi commented 1 year ago

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 so one does not detect the other. so i wrapper the 2 apps with a 1 and it works but its more complicated now because each one has its own actions and its own settings.

the question is, can detect draggable/droppable elements that's coming from another ?

Thank you

ehrro commented 1 year ago

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

alaabashiyi commented 1 year ago

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

ehrro commented 1 year ago

I will publish the code on sandbox.

vittoriozamboni commented 1 year ago

I had the same problem. I adopted a workaround using a placeholder component. This is my current situation:

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:

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.

alaabashiyi commented 1 year ago

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 (SortableTree component), 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 TreeItem component accepts a TreeItemWrapper, that by default it's just a Fragment (no-op);
  • added a new component SortableTreePlaceholder that 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 as TreeItemWrapper that is enriched with useDroppable;
  • use {active} = useDndContext() and render conditionally the components based on the value: if active is true, use SortableTreePlaceholder so the items from the right side can be dropped inside, otherwise render SortableTree that 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 !!

Swappea commented 6 months ago

I will publish the code on sandbox.

Hi, could you please share the code? Thank you

joacoespinosa commented 4 months ago

@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>