PhoenixKit.Utils.Tree (phoenix_kit v2.40.1)

Copy Markdown View Source

Nested trees for pickers (PhoenixKitWeb.Components.TreePicker): built from a flat, parent-linked list, and the pure operations a picker needs over them — so no module shows nested records as an indented flat list.

A node is %{id, name, children} plus optional keys:

  • :type — an atom the picker filters on (pickable, path_skip)
  • :icon — a heroicon name for the row
  • :hint — short text beside the name ("top level")
  • :badge — a small badge after the name ("Archived")

Any other key a module puts on a node rides along untouched.

Ids are strings. "root" (root_id/0) is the conventional id of a row standing for "no parent" — a picker posts it as "".

rows
|> Tree.from_flat(node: &%{name: &1.title, type: :page})
|> Tree.prune([record.uuid])
|> then(&[Tree.root(gettext("Top level"), &1)])

Summary

Functions

The ids of the rows above id, top first — what to open to show it.

The tree cut down to the rows whose name contains query (case and accents ignored) and the rows above them, plus the ids to open so every match shows. A matching row keeps its whole subtree, closed — finding a parent still lets the admin open it and pick a child. A blank query returns the tree untouched and opens nothing.

The row with id, or nil.

Builds the tree from a flat list of records linked by a parent id.

Every id in the tree whose row is one of types (:all for every row).

Applies fun to every node, children first.

Whether the tree offers id as a row of one of types — :all for any row. A picker checks a picked id with this before taking it.

The names from the top down to id. skip leaves rows of those types out of the path. [] when the tree lacks it.

For every row that is not one of types, the ids of the rows under it that are — in one pass, for a picker whose branch boxes check a whole branch.

The tree without the rows in ids and everything under them — a record cannot move into its own subtree.

A row standing for "no parent", named name, holding children. opts may set :hint and :icon (default "hero-home").

The id of the row that stands for "no parent".

Types

tree_node()

@type tree_node() :: %{
  :id => String.t(),
  :name => String.t(),
  :children => [tree_node()],
  optional(:type) => atom(),
  optional(:icon) => String.t(),
  optional(:hint) => String.t(),
  optional(:badge) => String.t(),
  optional(atom()) => term()
}

Functions

ancestor_ids(tree, id)

@spec ancestor_ids([tree_node()], term()) :: [String.t()]

The ids of the rows above id, top first — what to open to show it.

filter(tree, query)

@spec filter([tree_node()], String.t()) :: {[tree_node()], [String.t()]}

The tree cut down to the rows whose name contains query (case and accents ignored) and the rows above them, plus the ids to open so every match shows. A matching row keeps its whole subtree, closed — finding a parent still lets the admin open it and pick a child. A blank query returns the tree untouched and opens nothing.

find(tree, id)

@spec find([tree_node()], term()) :: tree_node() | nil

The row with id, or nil.

from_flat(records, opts \\ [])

@spec from_flat([term()], keyword()) :: [tree_node()]

Builds the tree from a flat list of records linked by a parent id.

A record whose parent is not in the list is a root — so a list that leaves out trashed rows keeps their children, one level up. Building walks down from the roots, so a corrupt parent cycle can never loop: nothing in a cycle is reachable from a root. Sibling order is the list's order.

Options

  • :id — the record's id (default & &1.uuid)
  • :parent — its parent's id or nil (default & &1.parent_uuid)
  • :node — the node's own keys (default &%{name: &1.name}); :id and :children are filled in

ids_of(tree, types \\ :all)

@spec ids_of([tree_node()], [atom()] | :all) :: [String.t()]

Every id in the tree whose row is one of types (:all for every row).

map_nodes(tree, fun)

@spec map_nodes([tree_node()], (tree_node() -> tree_node())) :: [tree_node()]

Applies fun to every node, children first.

member?(tree, id, types \\ :all)

@spec member?([tree_node()], term(), [atom()] | :all) :: boolean()

Whether the tree offers id as a row of one of types — :all for any row. A picker checks a picked id with this before taking it.

path(tree, id, skip \\ [])

@spec path([tree_node()], term(), [atom()]) :: [String.t()]

The names from the top down to id. skip leaves rows of those types out of the path. [] when the tree lacks it.

pickable_under(tree, types)

@spec pickable_under([tree_node()], [atom()] | :all) :: %{
  required(String.t()) => [String.t()]
}

For every row that is not one of types, the ids of the rows under it that are — in one pass, for a picker whose branch boxes check a whole branch.

prune(tree, ids)

@spec prune([tree_node()], [String.t()]) :: [tree_node()]

The tree without the rows in ids and everything under them — a record cannot move into its own subtree.

root(name, children, opts \\ [])

@spec root(String.t(), [tree_node()], keyword()) :: tree_node()

A row standing for "no parent", named name, holding children. opts may set :hint and :icon (default "hero-home").

root_id()

@spec root_id() :: String.t()

The id of the row that stands for "no parent".