tree/1 is a tree that people reorder and nest: pages, folders and
files, an outline. tree_picker/1 chooses a place in a tree, in a form.
<.tree id="pages" items={@pages} item_label={:title} item_kind={:kind} rules={Page} on_move="move" filter label="Pages" />The items
items is flat, as the database has it: each item with its parent's id
(item_parent, :parent_id by default), in their order. Read them with
Narabe.Ecto.ordered/2 and the tree shows each under its parent
(Narabe.Tree.rows/2). An item whose parent is not among the items is at
the top, so a tree may show one branch, or the levels loaded so far.
The schema says that its group is its parent:
defmodule MyApp.Docs.Page do
use Ecto.Schema
schema "pages" do
field :title, :string
field :kind, :string
field :position, :string
belongs_to :parent, __MODULE__
end
use Narabe.Ordered,
group: :parent_id,
tree: true,
kind: :kind,
rules: [nest: true, max_depth: 6, leaves: ["file"]]
endrules={Page} gives the tree the same rules as Narabe.Ecto (see the
Rules guide). A tree's rules nest when none are given.
The move
A move's "parent" is the new parent's id, or nil at the top level. The
item's subtree moves with it. The LiveView applies it as a list's:
def handle_event("move", params, socket) do
result = Narabe.Ecto.move(Repo, Page, Narabe.Move.parse!(params, Page))
{:reply, Narabe.Live.reply(result), load_pages(socket)}
endNarabe.Ecto refuses a move into the item itself or under it
(:into_itself), and checks the rules. In memory,
Narabe.Move.apply(pages, move, group: :parent_id, tree: true) does the
same.
Where a drop lands
With a pointer, the top quarter of a row is the gap before it, the bottom
quarter the gap after it, and the middle is inside it, when it may hold
children. A drop inside a closed row puts the items at the end of its
children. In a gap, moving the pointer left and right picks the depth,
one indent (--narabe-indent) per level, between what the rows above
and below allow.
With the keyboard, Space lifts, Up and Down pick the gap, Right goes one level deeper and Left one level up, Space drops.
Opening and closing
Rows open and close with a click on their arrow, or Right and Left.
expanded gives the ids open at first. From the server,
Narabe.Live.expand/4 opens or closes rows, and Narabe.Live.reveal/3
opens a row's ancestors, scrolls to it and focuses it.
Children loaded as they open
item_children says which items have children that are not loaded yet
(a key or a function). Such an item shows an arrow; as it opens, the
tree sends on_expand with %{"v" => 1, "id" => id} and shows a
spinner until its children are in items:
<.tree id="pages" items={@pages} item_children={:has_children} on_expand="expand" on_move="move" />def handle_event("expand", %{"id" => id}, socket) do
children = Page |> where(parent_id: ^id) |> Narabe.Ecto.ordered() |> Repo.all()
{:noreply, update(socket, :pages, &(&1 ++ children))}
endOrder matters only among siblings: the children may come after the rest.
Filter
With filter, a box above the tree shows the items whose label has the
text (case and accents do not count), with their ancestors dimmed for
context. The matched letters are marked. Nothing moves while the box has
text. Only the loaded items are searched.
Rename
With on_rename, F2 or a double click on a label opens a box in place.
Enter or leaving the box saves, Escape cancels. The tree shows the new
name at once and sends %{"v" => 1, "id" => id, "name" => name}:
def handle_event("rename", %{"id" => id, "name" => name}, socket) do
page = Repo.get!(Page, id)
{:ok, _} = page |> Ecto.Changeset.change(title: name) |> Repo.update()
{:noreply, load_pages(socket)}
endWith an item slot of your own, mark the element of the label with
data-narabe-label, so the rename box and the filter's marks find it:
<:item :let={page}>
<.icon name={icon(page)} />
<span data-narabe-label>{page.title}</span>
</:item>On a phone
A tree shows as a tree on a phone. With phone={:drill}, on a touch
screen 640 px wide or less, it shows one level at a time: a tap on a row
with children opens its level, and a bar at the top goes back. A held
finger reorders the items within the level shown.
The tree picker
tree_picker/1 is a field of a form that chooses an item of a tree: a
folder, a parent page, a category. It posts the item's id, as a
<select> would:
<.form for={@form} id="move-form" phx-change="validate" phx-submit="save">
<.tree_picker field={@form[:parent_id]} items={@folders} item_label={:title} disabled={[@page.id]} label="Folder" />
</.form>A click, Space or Enter picks an item. disabled ids, and the items
under them, show and cannot be picked (the page being moved, so it does
not go into itself). The chosen item's ancestors are open at first,
unless expanded says otherwise. The picker filters by default
(filter={false} turns it off) and drills on a phone by default
(phone={:tree} shows the tree). Nothing drags in a picker. It
dispatches narabe:pick in the page, with the id in its detail.
The tree in the database
Narabe.Ecto.ancestors/4 gives an item's ancestors' ids, parent first;
Narabe.Ecto.descendants/4 its descendants' ids. Each is one recursive
query. See the Database guide.