React-beautiful-dnd: Multiple Drop Type Support

Created on 25 Feb 2020  路  9Comments  路  Source: atlassian/react-beautiful-dnd

Description

We should be able to specify many different types that can be _accepted_ on a <Droppable/> component.

Use case example:

// 1. Specify the draggable components with different types
const RedItem = () => (<Droppable type="red">...</Droppable>)
const BlueItem = () => (<Droppable type="blue">...</Droppable>)
const YellowItem = () => (<Droppable type="yellow">...</Droppable>)

// 2. Specify the container which will only accept the different draggable
//    item types specified in the array
const acceptedTypes = ['red', 'blue']
const Palette = () => (
  <Droppable accepts={acceptedTypes}>
    ...
  </Droppable>
)

// 3. Sample Application: The Palette component should only accept the draggable
//    components which have 'red' or 'blue' types, much like how the _type_ prop
//    behaves but for more than 1 type.
const App = () => (
  <DragDropContext onDragEnd={() => null}>
   <Droppable droppableId="app">
     {provided => (
       <Container {...provided.droppableProps} ref={provided.innerRef}>
         <Items>
           <RedItem />
           <BlueItem />
           <YellowItem />
         </Items>
         <Palette />
         {provided.placeholder}
       </Container>
     )}
 </DragDropContext>
)

Motivation

Currently there's no way to define how to make <Droppable/> components accept more than 1 type and I think this is a very much requested feature (see below for related issues).

[Update] I've managed to find a way to do this with the current API. See here. Still though, I wish this kind of functionality was supported with a much easier API on this library...

Related Issues

P.S.

I'm willing to work on a PR for this if you'd like. But I'll need some guidance on what files I should touch or look upon.

idea 馃 untriaged

All 9 comments

Here is an Ad hoc solution for now (v13.0.0)

/**
 * 1. Specify the accepted types.
 */
const types = ['red', 'blue', 'green']

/**
 * 2. Create a nested Droppable component, each of which contains
 *    the different type that it can accept.
 */
const DropConditions = types.reduce((Wrapper, type, index) => ({children}) => (
  <Wrapper>
    <Droppable type={type} droppableId={`droppable-${index}`}>
      {provided => (
        <div
          {...provided.droppableProps}
          ref={provided.innerRef}
       >
          {children}
          {provided.placeholder}
        </div>
      )}    
    </Droppable>
  </Wrapper>
), React.Fragment)

/**
 *  3. Usage.
 */
const Palette = () => (
  <DropConditions>
    <Droppable
       droppableId="palette"
       /**
        * Optional:
        * Set this to true only for when you strictly want the specified types
        * to get in, else then even the 'DEFAULT' types will go through.
        */
       isDropDisabled={true}
    >
       ...
    </Droppable>
  </DropConditions>
)

So I have been working on my implementation using what we have from the api. I have a set of enums that namespace droppable and draggable ids and came up with a simple convention that the ids are namespaced by a comma.

eg.

// there are some places where many droppable contexts exist and so we need to discern between them
export const COMMON_DROPPABLE_IDS = {
  PACKAGE_BUILDER_TEST_SET_RECEIVER: 'Package.Builder.Test.Set.Receiver',
  PACKAGE_BUILDER_TEST_SET_EMITTER: 'Package.Builder.Test.Set.Emitter',
  PACKAGE_BUILD_PAGE_EMITTER: 'Package.Builder.Page.Emitter',
  PACKAGE_BUILD_PAGE_RECEIVER: 'Package.Builder.Page.RECEIVER',
};

export const COMMON_DRAGGABLE_IDS = {
  PACKAGE_BUILDER_TEST_SET_ITEM: 'Package.Builder.Test.Set.Item',
  PACKAGE_BUILDER_PAGE_TYPE: 'Package.Builder.Page.Type',
};

for almost all cases the ids need to be more unique and so draggable ids are typically something like

<Draggable
        index={index}
        draggableId={`${COMMON_DRAGGABLE_IDS.PACKAGE_BUILDER_TEST_SET_ITEM},${id}`}
      >
...

on my __DragNDropContext__ I have helpers within the drag events to _decode_ the ids to get the namespace and the id.

const handleDragUpdate = ({ draggableId, source, destination }) => {
    if (!source || !destination) return;

    const { droppableId } = extractResourceAndDropppableId(
      destination.droppableId,
    );
    const { droppableId: id } = extractResourceAndDropppableId(draggableId);

    setDropState({ whatsDragged: id, onto: droppableId });
  };

const handleDragStart = () => setDropState({whatsDragged: '', onto: ''});

  const handleDragEnd = ({ draggableId, source, destination }) => {
    if (!source || !destination) return;

    const { id, droppableId } = extractResourceAndDropppableId(
      destination.droppableId,
    );
   ...
}

Take note of the __handleDragUpdate__ and __handleDragStart__. I use those to provide a __means to conditionally disable droppables when the incorrect draggable is moved onto it__

eg

in one of my droppables..

const isDropDisabled =
      dropState.whatsDragged !== '' &&
      dropState.whatsDragged !==
        COMMON_DRAGGABLE_IDS.PACKAGE_BUILDER_TEST_SET_ITEM;

I refactored the namespacing so its easier to understand whats going on. It is all in a helper.

/**
 * extracts the namespaces from the drag and drop compponents and returns all drag an drop properties
 * plus the namespaces. the draggable id is the actual unique draggable id instead of it plus the namespace
 * @param {Object}  dropProps
 */
export const namespacedDragEvent = ({
  draggableId: originalDragId,
  source,
  destination,
}) => {
  const {
    id: draggableId,
    namespace: draggableNamespace,
  } = extractResourceAndNamespace(originalDragId);
  const {
    id: droppableId,
    namespace: droppableNamespace,
  } = extractResourceAndNamespace(destination.droppableId);

  const {
    id: sourceId,
    namespace: sourceNamespace,
  } = extractResourceAndNamespace(source.droppableId);

  return {
    source,
    destination,
    originalDragId,
    draggableId,
    draggableNamespace,
    droppableId,
    droppableNamespace,
    sourceId,
    sourceNamespace,
  };
};

unfortunately I cannot share the codebase I'm working on because its licenced but here is the gist of what i'm doing to tackle this.

@patricksimonian Interesting... I'll try this out later today. Thank you for showing how you worked on this! Looks better than my recursive solution.

@patricksimonian hey, so I tried this by doing a state update during the onDragStart where I would track the currently dragged item's type, like so:

function App() {
  const [dragType, setDragType] = useState(null)

  handleDragStart = drag => {
    setDragType(drag.type)
  }

  handleDragEnd = result => {
    ...
   setDragType(null)
  }
}

For some reason though rbd's throwing me errors that I shouldn't do state updates when I'm dragging. How did you make this work?

Nvm. Found a way, used onBeforeCapture and extracted the item's type on there to my state.

interesting I didn't have to do that. I think i used on drag start to reset and used on drag update to update the state.

From a design pattern I think namespacing droppables and draggables is very useful. I unfortunately don't have enough time to make big contributions (if the maintainers were interested) but i'd love to be apart of the conversation.

any thoughts for this from the maintainers of this project? The notion of namespaced drags and drops?

Thanks for the ideas on using the id for storing more state! I was able to handle my use case with context to pass around the parsed id attributes to child components that would then conditionally determine isDropDisabled for that item.

Was this page helpful?
0 / 5 - 0 ratings