Back to Reflex

Drag and Drop

docs/enterprise/drag-and-drop.md

0.9.2a216.7 KB
Original Source
python
import reflex as rx
import reflex_enterprise as rxe

Drag and Drop

Reflex Enterprise provides comprehensive drag and drop functionality for creating interactive UI elements using the rxe.dnd module. Built on top of react-dnd, it offers both high-level components for common use cases and low-level hooks for advanced scenarios.

md
# Important: Always decorate functions defining `rxe.dnd.draggable` components with `@rx.memo` to avoid compilation errors.

Basic Usage

Simple Drag and Drop

Here's a basic example showing how to create a draggable item and drop target:

python
import reflex as rx
import reflex_enterprise as rxe


class BasicDndState(rx.State):
    drop_count: int = 0

    def increment_drop_count(self):
        self.drop_count += 1


@rx.memo
def draggable_card():
    return rxe.dnd.draggable(
        rx.card(
            rx.text("Drag me!", weight="bold"),
            rx.text("I can be moved around"),
            bg="blue.500",
            color="white",
            p=4,
            cursor="grab",
            width="200px",
            height="100px",
        ),
        type="BasicCard",
        item={"message": "Hello from draggable!"},
    )


def basic_drag_drop():
    return rx.vstack(
        rx.text(f"Items dropped: {BasicDndState.drop_count}"),
        rx.hstack(
            draggable_card(),
            rxe.dnd.drop_target(
                rx.box(
                    "Drop Zone",
                    bg="gray.100",
                    border="2px dashed gray",
                    min_height="150px",
                    min_width="200px",
                    display="flex",
                    align_items="center",
                    justify_content="center",
                    font_weight="bold",
                ),
                accept=["BasicCard"],
                on_drop=BasicDndState.increment_drop_count,
            ),
            spacing="4",
            align="start",
        ),
        spacing="4",
    )

Multi-Position Drag and Drop

Create a draggable item that can be moved between multiple drop targets:

python
import reflex as rx
import reflex_enterprise as rxe


class MultiPositionState(rx.State):
    card_position: int = 0

    def set_card_position(self, position: int):
        self.card_position = position


@rx.memo
def movable_card():
    return rxe.dnd.draggable(
        rx.card(
            rx.text("Movable Card", weight="bold"),
            rx.text("Position: " + MultiPositionState.card_position.to_string()),
            bg="purple.500",
            color="white",
            p=4,
            width="180px",
            height="120px",
        ),
        type="MovableCard",
        border="2px solid purple",
    )


def drop_zone(position: int):
    params = rxe.dnd.DropTarget.collected_params
    return rxe.dnd.drop_target(
        rx.cond(
            MultiPositionState.card_position == position,
            movable_card(),
            rx.box(f"Drop Zone {position}", color="gray.600", font_weight="bold"),
        ),
        width="200px",
        height="200px",
        border="2px solid red",
        border_color=rx.cond(params.is_over, "green.500", "red.500"),
        bg=rx.cond(params.is_over, "green.100", "blue.100"),
        accept=["MovableCard"],
        on_drop=lambda _: MultiPositionState.set_card_position(position),
        display="flex",
        align_items="center",
        justify_content="center",
    )


def multi_position_example():
    return rx.vstack(
        rx.text("Drag the card between positions", weight="bold"),
        rx.grid(
            drop_zone(0),
            drop_zone(1),
            drop_zone(2),
            drop_zone(3),
            columns="2",
            spacing="4",
        ),
        spacing="4",
    )

Advanced Features

State Tracking with Collected Parameters

Access drag and drop state information using collected parameters:

python
import reflex as rx
import reflex_enterprise as rxe


class StateTrackingState(rx.State):
    drag_info: str = "No drag activity"

    def set_drag_info(self, value: str):
        self.drag_info = value


@rx.memo
def tracked_draggable():
    drag_params = rxe.dnd.Draggable.collected_params
    return rxe.dnd.draggable(
        rx.card(
            rx.text("Tracked Draggable"),
            rx.text(rx.cond(drag_params.is_dragging, "Dragging...", "Ready to drag")),
            bg=rx.cond(drag_params.is_dragging, "orange.500", "blue.500"),
            color="white",
            p=4,
            opacity=rx.cond(drag_params.is_dragging, 0.5, 1.0),
        ),
        type="TrackedItem",
        on_end=StateTrackingState.set_drag_info("Drag ended"),
    )


def tracked_drop_target():
    drop_params = rxe.dnd.DropTarget.collected_params
    return rxe.dnd.drop_target(
        rx.box(
            rx.text("Smart Drop Zone"),
            rx.text(rx.cond(drop_params.is_over, "Ready to receive!", "Waiting...")),
            bg=rx.cond(drop_params.is_over, "green.200", "gray.100"),
            border=rx.cond(drop_params.is_over, "2px solid green", "2px dashed gray"),
            p=4,
            min_height="150px",
            display="flex",
            flex_direction="column",
            align_items="center",
            justify_content="center",
        ),
        accept=["TrackedItem"],
        on_drop=StateTrackingState.set_drag_info("Item successfully dropped!"),
        on_hover=StateTrackingState.set_drag_info("Item hovering over drop zone"),
    )


def state_tracking_example():
    return rx.vstack(
        rx.text(f"Status: {StateTrackingState.drag_info}"),
        rx.hstack(tracked_draggable(), tracked_drop_target(), spacing="4"),
        spacing="4",
    )

Dynamic Lists with Drag and Drop

Create dynamic draggable lists using rx.foreach:

python
import dataclasses
import reflex as rx
import reflex_enterprise as rxe


@dataclasses.dataclass
class ListItem:
    id: str
    text: str
    list_id: str


class DynamicListState(rx.State):
    list_a: list[ListItem] = [
        ListItem(id="1", text="Item 1", list_id="A"),
        ListItem(id="2", text="Item 2", list_id="A"),
        ListItem(id="3", text="Item 3", list_id="A"),
    ]
    list_b: list[ListItem] = [
        ListItem(id="4", text="Item 4", list_id="B"),
        ListItem(id="5", text="Item 5", list_id="B"),
    ]

    def move_item(self, item_data: dict, target_list: str):
        item_id = item_data.get("id")
        source_list = item_data.get("list_id")

        if not item_id or not source_list:
            return

        # Find the item in the source list
        source_items = getattr(self, f"list_{source_list.lower()}")
        item_to_move = None
        for item in source_items:
            if item.id == item_id:
                item_to_move = item
                break

        if not item_to_move:
            return

        # Remove from source list only
        if source_list == "A":
            self.list_a = [item for item in self.list_a if item.id != item_id]
        else:
            self.list_b = [item for item in self.list_b if item.id != item_id]

        # Create new item for target list
        new_item = ListItem(id=item_id, text=item_to_move.text, list_id=target_list)

        # Add to target list
        if target_list == "A":
            self.list_a.append(new_item)
        else:
            self.list_b.append(new_item)


@rx.memo
def draggable_list_item(item: ListItem):
    return rxe.dnd.draggable(
        rx.card(
            rx.text(item.text, weight="bold"),
            rx.text(f"From List {item.list_id}", size="2", color="gray.600"),
            p=3,
            cursor="grab",
            _hover={"bg": "gray.50"},
        ),
        type="ListItem",
        item={"id": item.id, "text": item.text, "list_id": item.list_id},
    )


def droppable_list(title: str, items: list[ListItem], list_id: str):
    return rxe.dnd.drop_target(
        rx.vstack(
            rx.text(title, weight="bold", size="5"),
            rx.vstack(
                rx.foreach(items, lambda item, index: draggable_list_item(item=item)),
                spacing="2",
                min_height="200px",
                width="100%",
            ),
            bg="gray.50",
            p=4,
            border_radius="md",
            border="2px dashed gray",
            width="250px",
        ),
        accept=["ListItem"],
        on_drop=lambda item: DynamicListState.move_item(item, list_id),
    )


def dynamic_list_example():
    return rx.hstack(
        droppable_list("List A", DynamicListState.list_a, "A"),
        droppable_list("List B", DynamicListState.list_b, "B"),
        spacing="6",
        align="start",
    )

Core Components

Draggable

The rxe.dnd.draggable component makes any element draggable:

Key Properties:

  • type: String identifier for drag type matching
  • item: Data object passed to drop handlers
  • on_end: Called when drag operation ends

Drop Target

The rxe.dnd.drop_target component creates areas that accept draggable items:

Key Properties:

  • accept: List of drag types this target accepts
  • on_drop: Called when item is dropped
  • on_hover: Called when item hovers over target

Collected Parameters

Access real-time drag/drop state:

Draggable Parameters (rxe.dnd.Draggable.collected_params):

  • is_dragging: Boolean indicating if item is being dragged

Drop Target Parameters (rxe.dnd.DropTarget.collected_params):

  • is_over: Boolean indicating if draggable is hovering
  • can_drop: Boolean indicating if drop is allowed

API Reference

rxe.dnd.draggable

Creates a draggable component that can be moved around the interface.

Parameters:

  • type (str, required): String identifier that must match the accept list of drop targets
  • item (dict | Callable): Data object passed to drop handlers. Can be a static dictionary or a function that receives a DragSourceMonitor and returns data
  • preview_options (dict): Configuration for the drag preview appearance
  • options (dict): Additional drag source options like dropEffect
  • on_end (EventHandler): Event handler called when drag operation completes
  • can_drag (Callable): Function that determines if the item can be dragged
  • is_dragging (Callable): Function to override the default dragging state detection
  • collect (Callable): Function to collect custom properties from the drag monitor

rxe.dnd.drop_target

Creates a drop target that can receive draggable items.

Parameters:

  • accept (str | list[str], required): Drag type(s) this target accepts
  • options (dict): Additional drop target configuration options
  • on_drop (EventHandler): Event handler called when an item is dropped, receives the item data
  • on_hover (EventHandler): Event handler called when an item hovers over the target
  • can_drop (Callable): Function that determines if a specific item can be dropped
  • collect (Callable): Function to collect custom properties from the drop monitor

Monitor Classes

DragSourceMonitor

Provides information about the drag operation state:

  • is_dragging(): Returns True if this item is currently being dragged
  • can_drag(): Returns True if the item can be dragged
  • get_item(): Returns the item data being dragged
  • get_item_type(): Returns the drag type string
  • get_drop_result(): Returns the drop result (available in on_end)
  • did_drop(): Returns True if the item was successfully dropped

DropTargetMonitor

Provides information about the drop target state:

  • is_over(): Returns True if a draggable item is hovering over this target
  • can_drop(): Returns True if the hovering item can be dropped
  • get_item(): Returns the item data of the hovering draggable
  • get_item_type(): Returns the drag type of the hovering item

Default Collected Parameters

Draggable.collected_params

python
{
    "is_dragging": bool,  # True when this item is being dragged
    "can_drag": bool,  # True when this item can be dragged
}

DropTarget.collected_params

python
{
    "is_over": bool,  # True when a draggable is hovering
    "can_drop": bool,  # True when the hovering item can be dropped
    "item": dict | None,  # Data from the hovering draggable item
}

Advanced Usage Examples

Data Passing with Item Parameter

The item parameter allows you to pass data from draggable components to drop handlers:

python
import reflex as rx
import reflex_enterprise as rxe


class SimpleState(rx.State):
    message: str = "No items dropped yet"

    def set_message_from_item(self, item: dict):
        self.message = f"Dropped: {item['name']}"


def simple_draggable():
    return rxe.dnd.draggable(
        rx.box("Drag me!", p=4, bg="blue.100", border="1px solid blue", cursor="grab"),
        type="simple",
        item={"name": "test_item", "value": 42},
    )


def simple_drop_target():
    return rxe.dnd.drop_target(
        rx.box(
            rx.text(SimpleState.message),
            p=4,
            bg="gray.100",
            border="2px dashed gray",
            min_height="100px",
        ),
        accept=["simple"],
        on_drop=SimpleState.set_message_from_item,
    )


def item_data_example():
    return rx.vstack(simple_draggable(), simple_drop_target(), spacing="4")

Custom Collect Functions

The collect parameter allows you to access drag and drop state information in real-time:

python
import reflex as rx
import reflex_enterprise as rxe


class CollectState(rx.State):
    drag_info: str = "No drag activity"
    drop_info: str = "No drop activity"

    def handle_drop(self, item: dict):
        self.drop_info = f"Dropped: {item.get('name', 'Unknown')}"
        return rx.toast(f"Successfully dropped {item.get('name', 'item')}")


def collect_draggable():
    params = rxe.dnd.Draggable.collected_params
    return rxe.dnd.draggable(
        rx.box(
            rx.vstack(
                rx.text("Drag me!", weight="bold"),
                rx.text(f"Dragging: {params.is_dragging}", size="2"),
                rx.text(f"Can drag: {params.can_drag}", size="2"),
                spacing="1",
            ),
            p=4,
            bg=rx.cond(params.is_dragging, "blue.200", "blue.100"),
            border="1px solid blue",
            cursor=rx.cond(params.is_dragging, "grabbing", "grab"),
            opacity=rx.cond(params.is_dragging, 0.7, 1.0),
        ),
        type="collect_item",
        item={"id": "collect_test", "name": "Test Item"},
    )


def collect_drop_target():
    params = rxe.dnd.DropTarget.collected_params
    return rxe.dnd.drop_target(
        rx.box(
            rx.vstack(
                rx.text("Drop Zone", weight="bold"),
                rx.text(f"Is over: {params.is_over}", size="2"),
                rx.text(f"Can drop: {params.can_drop}", size="2"),
                rx.cond(
                    params.item,
                    rx.text(
                        f"Hovering item: {params.item.get('name', 'Unknown')}", size="2"
                    ),
                    rx.text("No item hovering", size="2"),
                ),
                spacing="1",
            ),
            p=4,
            bg=rx.cond(
                params.is_over & params.can_drop,
                "green.200",
                rx.cond(params.is_over, "yellow.200", "gray.100"),
            ),
            border=rx.cond(
                params.is_over & params.can_drop,
                "2px solid green",
                rx.cond(params.is_over, "2px solid yellow", "2px dashed gray"),
            ),
            min_height="120px",
        ),
        accept=["collect_item"],
        on_drop=CollectState.handle_drop,
    )


def custom_collect_example():
    return rx.vstack(
        rx.text("Real-time Monitor State", weight="bold", size="4"),
        rx.hstack(
            collect_draggable(), collect_drop_target(), spacing="6", align="start"
        ),
        rx.text(CollectState.drop_info, size="2", color="gray.600"),
        spacing="4",
    )

Provider

Drag and drop functionality requires the rxe.dnd.provider component to wrap your app. The provider is automatically added when using draggable or drop_target components.

For manual control:

python
def app():
    return rxe.dnd.provider(
        # Your app content
        your_app_content(),
        backend="HTML5",  # or "Touch" for mobile
    )

Best Practices

  1. Always use @rx.memo on functions containing draggable components
  2. Use descriptive type names for better debugging
  3. Handle edge cases in drop handlers (invalid items, etc.)
  4. Provide visual feedback using collected parameters
  5. Test on mobile devices with touch backend
  6. Keep item data lightweight for better performance

← Back to main documentation