Drag and drop
Reordering a list, dragging a card from one column to another. The browser makes the gesture, the server receives the result — and it is the server that decides whether to apply it.The two halves
`ui.dropzone` is the region that accepts; `ui.draggable` wraps an item one can grab. Both are CONTAINERS: their content is written inside a `with`.with ui.dropzone(name="todo", accepts=["task"],
on_move=move_task):
for t in ui.each(tasks, key="id"):
with ui.draggable(key=str(t.id), group="task"):
ui.card(t.title)
Entry is explicit
A zone always lets its own cards be reordered. To receive a card from another zone, list its `group=` in `accepts=`. With no `accepts=`, unrelated zones cannot exchange cards.
Four drop behaviors
| Mode | During the drag |
|---|---|
| Reorder in one zone | The card moves between its neighbors; no `accepts=` is needed. |
| Move between zones | The destination lists the card's `group=` in `accepts=`. |
| Single place | `holds="one"` previews replacement without inserting a second card. |
| Action target | `terminal=True` reports the drop without inserting the card; use it for archive or delete. |
What the handler receives
A single object, `Move`, with five fields. It says everything needed to apply OR refuse the gesture — and refusing is a normal case.| Field | What it says |
|---|---|
| item_key | the moved item's key — the `ui.draggable(key=…)`'s |
| from_zone | the departure zone's `name=` |
| to_zone | the arrival zone's `name=` |
| from_index | its original position in the zone |
| to_index | the position aimed at on arrival |
from bretzel.components import Move
def on_drop(move: Move) -> None:
"""What a drop applies — or refuses."""
if not move.to_zone.startswith("column-"):
return # refused, without a word
task_id = int(move.item_key)
move_in_database(task_id, move.to_zone,
move.to_index)
`item_key` is a STRING — that is what the DOM carries. A numeric key converts back on arrival, and an `int()` that raises on an unexpected value is better than a move made at random.Controlling entry and exit
`accepts=` controls entry from another zone. `locked=True` prevents cards from leaving a zone; it can still receive cards. For an archive, combine `accepts=`, `locked=True`, and `terminal=True`. The handler applies or refuses the action.with ui.dropzone(name="archive", accepts=["task"],
locked=True, terminal=True,
on_move=archive_task):
...