dnd/groups
Types
Configuration for group-aware drag-and-drop behavior.
This extends the basic drag-and-drop configuration to support items organized in logical groups. It defines separate behaviors for same-group operations (using the main config) and cross-group operations (using the groups config).
Fields
before_update: A callback function that allows you to modify the list before any operation is appliedmovement: Constrains how the ghost element can move (Free, Horizontal, or Vertical)listen: When to apply same-group changes (OnDrag for real-time, OnDrop for final placement)operation: What operation to perform for same-group transfersgroups: Configuration for cross-group operations (see GroupsConfig)
Behavior
- When dragging within the same group: uses
listenandoperation - When dragging between different groups: uses
groups.listenandgroups.operation - The
groups.comparatordetermines whether items are in the same group - The
groups.setterupdates the item when it changes groups
Example
let config = groups.Config(
before_update: fn(_, _, list) { list },
movement: groups.Free, // Allow movement in any direction
listen: groups.OnDrag, // Same-group: update while dragging
operation: groups.Rotate, // Same-group: rotate items
groups: groups.GroupsConfig(
listen: groups.OnDrag, // Cross-group: update while dragging
operation: groups.InsertBefore, // Cross-group: insert before target
comparator: fn(a, b) { a.group == b.group },
setter: fn(target, drag) { Item(..drag, group: target.group) },
),
)
pub type Config(a) {
Config(
before_update: fn(Int, Int, List(a)) -> List(a),
movement: Movement,
listen: Listen,
operation: Operation,
groups: GroupsConfig(a),
)
}
Constructors
-
Config( before_update: fn(Int, Int, List(a)) -> List(a), movement: Movement, listen: Listen, operation: Operation, groups: GroupsConfig(a), )
Configuration for cross-group drag-and-drop operations.
This type defines how items should behave when dragged between different logical groups. It works alongside the main Config to provide different behaviors for same-group vs cross-group transfers.
Fields
listen: When to apply cross-group changes (OnDrag for real-time, OnDrop for final placement)operation: What operation to perform when transferring between groups (InsertAfter, InsertBefore, Rotate, Swap, or Unaltered)comparator: Function to determine if two items belong to the same group. ReturnTrueif items are in the same group.setter: Function to update the dragged item when it’s moved to a different group. Receives (target_item, drag_item) and returns the updated drag_item.
Example
type Group { Left | Right }
type Item { Item(group: Group, value: String) }
let groups_config = groups.GroupsConfig(
listen: groups.OnDrag,
operation: groups.InsertBefore,
comparator: fn(a, b) { a.group == b.group }, // Same group if group field matches
setter: fn(target, drag) { Item(..drag, group: target.group) }, // Update group membership
)
pub type GroupsConfig(a) {
GroupsConfig(
listen: Listen,
operation: Operation,
comparator: fn(a, a) -> Bool,
setter: fn(a, a) -> a,
)
}
Constructors
Information about the current drag operation when an item is being dragged.
This type provides access to all relevant details about an active drag operation, including positions, indices, and element identifiers. In the groups context, this information applies regardless of whether the drag is within the same group or between different groups.
Fields
drag_index: List index of the item being dragged (original position)drop_index: List index of the current drop target (where it would be placed)drag_element_id: DOM element ID of the dragged itemdrop_element_id: DOM element ID of the current drop targetstart_position: Mouse coordinates where the drag begancurrent_position: Current mouse coordinates during drag
Group Operations
The indices provided here are absolute positions in the full list, regardless of group membership. The system internally handles group logic using the comparator function to determine same-group vs cross-group behavior.
Usage
// Check if currently dragging and get info
case model.system.info(model.system.model) {
Some(info) -> {
// Currently dragging - can access both drag and drop information
let drag_item = list.drop(model.items, info.drag_index) |> list.take(1)
let drop_item = list.drop(model.items, info.drop_index) |> list.take(1)
// Use for group-aware styling, validation, etc.
}
None -> {
// Not currently dragging
}
}
Availability
This information is only available during active drag operations (returns Some(Info)).
When no drag is in progress, system.info() returns None.
pub type Info {
Info(
drag_index: Int,
drop_index: Int,
drag_element_id: String,
drop_element_id: String,
start_position: Position,
current_position: Position,
)
}
Constructors
Determines when list modifications should be applied during drag operations.
This controls the timing of when your list gets updated - either continuously while dragging for real-time visual feedback, or only when the item is dropped for final placement. In the groups module, this applies to both same-group and cross-group operations independently.
Variants
OnDrag: Apply list changes immediately as the item is dragged over drop targets. Provides real-time visual feedback but triggers more frequent updates.OnDrop: Apply list changes only when the drag operation completes. More performant for complex lists but less visual feedback during dragging.
Group Context
You can configure different listen modes for same-group vs cross-group operations:
- Main
listen: Controls when same-group operations are applied groups.listen: Controls when cross-group operations are applied
Example
let config = groups.Config(
listen: groups.OnDrag, // Same-group: immediate updates
groups: groups.GroupsConfig(
listen: groups.OnDrop, // Cross-group: only on drop
// ... other group options
),
// ... other options
)
pub type Listen {
OnDrag
OnDrop
}
Constructors
-
OnDrag -
OnDrop
Defines how the list should be modified when an item is dragged and dropped.
Each operation provides a different way of rearranging items in the list, affecting how the dragged item and other items are repositioned. In the context of groups, operations work differently for same-group vs cross-group transfers.
Variants
InsertAfter: Move the dragged item to the position immediately after the drop targetInsertBefore: Move the dragged item to the position immediately before the drop targetRotate: Perform a circular shift of all items between the drag and drop positions (includes both endpoints)Swap: Exchange the positions of the dragged item and the drop target itemUnaltered: Do not modify the list (useful for custom logic or visual feedback only)
Group Behavior
For same-group operations, this behaves identically to the basic dnd module.
For cross-group operations, the dragged item’s group membership is updated using
the setter function before applying the positional change.
Examples
Given items with groups [A₁, B₁, C₂, D₂, E₁] (subscript = group) and dragging C₂ to E₁:
InsertAfter:[A₁, B₁, D₂, E₁, C₁]- C changes to group 1 and moves after EInsertBefore:[A₁, B₁, D₂, C₁, E₁]- C changes to group 1 and moves before ERotate: Group-aware rotation with group membership updatesSwap:[A₁, B₁, E₂, D₂, C₁]- C and E exchange both positions and groups
pub type Operation {
InsertAfter
InsertBefore
Rotate
Swap
Unaltered
}
Constructors
-
InsertAfter -
InsertBefore -
Rotate -
Swap -
Unaltered
pub type Position {
Position(x: Float, y: Float)
}
Constructors
-
Position(x: Float, y: Float)
The group-aware drag-and-drop system containing all necessary functions and state.
This type extends the basic drag-and-drop system to support items organized in logical groups.
It provides intelligent behavior for same-group vs cross-group operations.
Create it using groups.create() and integrate it into your Lustre application.
Type Parameters
a: The type of items in your draggable list (must support group operations)msg: Your application’s message type
Fields
model: Internal drag state (opaque - useinfo()to access current state)update: Function to handle drag messages with group-aware logic for both same-group and cross-group operationsdrag_events: Function to generate mouse event attributes for draggable elementsdrop_events: Function to generate mouse event attributes for drop targetsghost_styles: Function to generate CSS styling for the ghost elementinfo: Function to extract current drag information (positions, indices, etc.)
Group Behavior
The system automatically detects whether a drag operation is within the same group or between different groups, and applies the appropriate configuration:
- Same group: Uses main
operationandlistensettings - Cross group: Uses
groups.operationandgroups.listensettings, plussetterto update group membership
Usage
// In your update function:
DndMsg(dnd_msg) -> {
let #(new_dnd, new_items) =
model.system.update(dnd_msg, model.system.model, model.items)
let updated_system = groups.System(..model.system, model: new_dnd)
Model(system: updated_system, items: new_items)
}
// In your view function:
html.div(
[..model.system.drag_events(index, element_id)],
[html.text(item.value)]
)
pub type System(a, msg) {
System(
model: Model,
update: fn(DndMsg, Model, List(a)) -> #(Model, List(a)),
drag_events: fn(Int, String) -> List(@internal Attribute(msg)),
drop_events: fn(Int, String) -> List(@internal Attribute(msg)),
ghost_styles: fn(Model) -> List(@internal Attribute(msg)),
info: fn(Model) -> option.Option(Info),
)
}
Constructors
-
System( model: Model, update: fn(DndMsg, Model, List(a)) -> #(Model, List(a)), drag_events: fn(Int, String) -> List(@internal Attribute(msg)), drop_events: fn(Int, String) -> List(@internal Attribute(msg)), ghost_styles: fn(Model) -> List(@internal Attribute(msg)), info: fn(Model) -> option.Option(Info), )