Packages
Lists, trees and boards that people reorder, for Phoenix LiveView: drag, keyboard and phones, with the order kept right in your database.
Current section
Files
Jump to
Current section
Files
guides/lists.md
# Lists
`sortable_list/1` is a list that people reorder: tasks, a playlist, the
links of a menu.
```heex
<.sortable_list id="tasks" items={@streams.tasks} on_move="move" multiple label="Tasks">
<:item :let={task}>{task.title}</:item>
</.sortable_list>
```
## The items
`items` is a list or a stream. Each item is a struct or a map. The list
reads its id from `:id`, and its label from `:name`, `:title` or `:label`;
`item_id` and `item_label` take another key or a function:
```heex
<.sortable_list id="songs" items={@songs} on_move="move" item_id={& &1.uuid} item_label={:song_name} />
```
The label is what a screen reader hears as the item moves, and what
shows when there is no `item` slot. Give the list a `label` too: it names
the whole list.
## The move
A drop sends `on_move` to the LiveView, or to the component of the
list's `phx-target`:
```elixir
%{"v" => 1, "ids" => ["7"], "parent" => nil, "after" => "3", "before" => "9", "index" => 2}
```
The items moved (several when selected), and where: right after one
item, right before another, at an index among the new neighbours. Ids are
text. `Narabe.Move.parse/2` reads the params into a `Narabe.Move` with
the ids cast to the type of a schema's primary key (or to an Ecto type
such as `:id`, `:binary_id` or `:string`). `Narabe.Move.parse!/2` raises
for params that are no move.
The place is said by ids, not only by an index, so that a screen a moment
out of date still means what the person saw. `after` wins over `before`;
`index` is for a neighbour that is gone.
```elixir
def handle_event("move", params, socket) do
move = Narabe.Move.parse!(params, Task)
result = Narabe.Ecto.move(Repo, Task, move)
{:reply, Narabe.Live.reply(result), socket}
end
```
The browser has moved the items already. Reply `%{ok: true}` to keep
them there, or `%{error: reason}` to put them back. `Narabe.Live.reply/1`
builds the reply from `{:ok, _}`, `{:error, atom}`, a failed
`Ecto.Multi`, or `{:error, "a message"}`, which the person hears. A drop
where the items already were sends nothing.
## Keeping the order yourself
`Narabe.Move.apply/3` applies a move to a list in memory, for an app
that keeps the order without `Narabe.Ecto`:
```elixir
def handle_event("move", params, socket) do
move = Narabe.Move.parse!(params, :id)
case Narabe.Move.apply(socket.assigns.tasks, move) do
{:ok, tasks} -> {:reply, %{ok: true}, assign(socket, tasks: tasks)}
error -> {:reply, Narabe.Live.reply(error), socket}
end
end
```
## Streams and other viewers
With a stream, the person who moved an item sees it in its place already.
Tell the other viewers, and put it in its place in their stream with
`Narabe.Live.stream_move/4`:
```elixir
def handle_event("move", params, socket) do
move = Narabe.Move.parse!(params, Task)
result = Narabe.Ecto.move(Repo, Task, move)
with {:ok, tasks} <- result do
Phoenix.PubSub.broadcast_from(MyApp.PubSub, self(), "tasks", {:moved, tasks, move})
end
{:reply, Narabe.Live.reply(result), socket}
end
def handle_info({:moved, tasks, move}, socket) do
{:noreply, Narabe.Live.stream_move(socket, :tasks, tasks, move)}
end
```
Subscribe to the topic in `mount/3` when connected. Insert and delete in
the stream as usual.
## Selecting several
With `multiple`, a click selects an item, Shift+click a range and
Ctrl+click (Cmd+click) one more; Shift+arrows extend the selection,
Ctrl+A (Cmd+A) selects all, Escape clears it. Dragging or lifting a
selected item moves every selected item, in their order.
`on_select` sends `%{"v" => 1, "ids" => [...]}` when the selection
changes. `Narabe.Live.select/3` selects items from the server, with no
event back.
## Handles
By default an item drags from anywhere on it. With `handle`, each item
shows a handle, and a mouse starts a drag only there; a finger still
holds the item anywhere, since a handle is small under a finger.
A drag never starts on a link, a button, a form field, a label, editable
text, or an element with `data-narabe-nodrag`, so they keep their clicks.
## Locked items
`item_locked` gives a text for an item that may not move, or nil. The
item shows a lock and cannot be lifted; the person hears the `"locked"`
message ("{name} is moving this") with the text as `{name}`. Change that
message with `messages` if your text is not a name.
## Swipe actions
The `actions` slot holds buttons that a finger swipes into view on a
phone: a swipe left shows them, past half their width they stay open. On
a mouse they are hidden.
```heex
<:actions :let={task}>
<button type="button" phx-click="done" phx-value-id={task.id}>Done</button>
</:actions>
```
## Read-only
`readonly`, or no `on_move`, keeps focus and selection and moves
nothing.
## Events in the page
The list dispatches DOM events that bubble: `narabe:move` (its `detail`
is the move sent) and `narabe:refused` (the server's error reply).
## Without LiveView
On a page without a LiveSocket, `mountNarabe()` mounts the lists: they
focus, select, filter and expand, but a move has no server to go to.
```js
import { mountNarabe } from "narabe"
window.addEventListener("DOMContentLoaded", () => mountNarabe())
```