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 applied
  • listen: When to apply same-group changes (OnDrag for real-time, OnDrop for final placement)
  • operation: What operation to perform for same-group transfers
  • groups: Configuration for cross-group operations (see GroupsConfig)

Behavior

  • When dragging within the same group: uses listen and operation
  • When dragging between different groups: uses groups.listen and groups.operation
  • The groups.comparator determines whether items are in the same group
  • The groups.setter updates the item when it changes groups

Example

let config = groups.Config(
  before_update: fn(_, _, list) { list },
  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),
    listen: Listen,
    operation: Operation,
    groups: GroupsConfig(a),
  )
}

Constructors

pub type DndMsg {
  DragStart(Int, String, Position)
  Drag(Position)
  DragOver(Int, String)
  DragEnter(Int)
  DragLeave
  DragEnd
}

Constructors

  • DragStart(Int, String, Position)
  • Drag(Position)
  • DragOver(Int, String)
  • DragEnter(Int)
  • DragLeave
  • DragEnd

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. Return True if 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

  • GroupsConfig(
      listen: Listen,
      operation: Operation,
      comparator: fn(a, a) -> Bool,
      setter: fn(a, a) -> a,
    )

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 item
  • drop_element_id: DOM element ID of the current drop target
  • start_position: Mouse coordinates where the drag began
  • current_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

  • Info(
      drag_index: Int,
      drop_index: Int,
      drag_element_id: String,
      drop_element_id: String,
      start_position: Position,
      current_position: Position,
    )

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
pub type Model {
  Model(option.Option(State))
}

Constructors

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 target
  • InsertBefore: Move the dragged item to the position immediately before the drop target
  • Rotate: 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 item
  • Unaltered: 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 E
  • InsertBefore: [A₁, B₁, D₂, C₁, E₁] - C changes to group 1 and moves before E
  • Rotate: Group-aware rotation with group membership updates
  • Swap: [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)
pub type State {
  State(
    drag_index: Int,
    drop_index: Int,
    drag_counter: Int,
    start_position: Position,
    current_position: Position,
    drag_element_id: String,
    drop_element_id: String,
  )
}

Constructors

  • State(
      drag_index: Int,
      drop_index: Int,
      drag_counter: Int,
      start_position: Position,
      current_position: Position,
      drag_element_id: String,
      drop_element_id: String,
    )

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 - use info() to access current state)
  • update: Function to handle drag messages with group-aware logic for both same-group and cross-group operations
  • drag_events: Function to generate mouse event attributes for draggable elements
  • drop_events: Function to generate mouse event attributes for drop targets
  • ghost_styles: Function to generate CSS styling for the ghost element
  • info: 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 operation and listen settings
  • Cross group: Uses groups.operation and groups.listen settings, plus setter to 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),
    )

Values

pub fn create(
  config: Config(a),
  step_msg: fn(DndMsg) -> msg,
) -> System(a, msg)
Search Document