Ensaimada
A drag-and-drop sortable list library for Gleam/Lustre with full support for both desktop and mobile devices.
Features
- Desktop Support: HTML5 drag and drop API
- Mobile Support: Touch events for drag and drop on mobile devices
- Customizable: Configurable CSS classes for styling
- Type Safe: Fully typed with Gleam’s type system
- Framework Agnostic: No built-in CSS framework dependencies
- Cross-Container Support: Move items between multiple sortable containers
Installation
Add ensaimada to your Gleam project:
gleam add ensaimada
Quick Start
import gleam/list
import lustre
import lustre/element.{type Element}
import lustre/element/html
import ensaimada
pub type Model {
  Model(items: List(String), drag_state: ensaimada.DragState)
}
pub type Msg {
  SortableMsg(ensaimada.Msg(Msg))
  Reorder(Int, Int)
}
pub fn update(model: Model, msg: Msg) -> Model {
  case msg {
    SortableMsg(sortable_msg) -> {
      let config = ensaimada.default_config(Reorder, "my-list")
      let #(new_drag_state, maybe_action) =
        ensaimada.update(sortable_msg, model.drag_state, config)
      case maybe_action {
        option.Some(ensaimada.SameContainer(from, to)) -> {
          let new_items = ensaimada.reorder(model.items, from, to)
          Model(..model, items: new_items, drag_state: new_drag_state)
        }
        _ -> Model(..model, drag_state: new_drag_state)
      }
    }
    Reorder(_, _) -> model
  }
}
pub fn view(model: Model) -> Element(Msg) {
  let config = ensaimada.default_config(Reorder, "my-list")
  let sortable_items =
    list.index_map(model.items, fn(item, i) {
      ensaimada.item("item-" <> int.to_string(i), item)
    })
  ensaimada.container(
    config,
    model.drag_state,
    sortable_items,
    fn(item, _index, _drag_state) {
      html.div([], [html.text(ensaimada.item_data(item))])
    },
  )
  |> element.map(SortableMsg)
}
Styling
The library uses CSS classes for styling. Here’s an example CSS setup:
.sortable-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
  padding: 1rem;
}
.sortable-item {
  padding: 1rem;
  background: white;
  border: 2px solid #e5e7eb;
  border-radius: 0.5rem;
  transition: all 0.2s ease;
}
.sortable-dragging {
  opacity: 0.5;
  transform: scale(1.05) rotate(3deg);
}
.sortable-drag-over {
  border-color: #3b82f6;
  background: #eff6ff;
}
.sortable-active {
  user-select: none;
}
Cross-Container Drag and Drop
To enable dragging between multiple containers:
let config1 = ensaimada.Config(
  ..ensaimada.default_config(Reorder, "container-1"),
  accept_from: ["container-2"]
)
let config2 = ensaimada.Config(
  ..ensaimada.default_config(Reorder, "container-2"),
  accept_from: ["container-1"]
)
Then handle CrossContainer actions in your update function:
case maybe_action {
  Some(ensaimada.CrossContainer(from_cont, from_idx, to_cont, to_idx)) -> {
    // Remove from source container and add to target container
    ...
  }
  ...
}
Testing
Run tests with:
gleam test
License
This project is available under the MIT license. See LICENSE for details.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.