Basic Usage
Basic usage with checkable, selectable, disabled nodes, and default keys.
Code
<% nodes = [
{
key: "0-0",
title: "Parent Node",
children: [
{ key: "0-0-0", title: "Child Node", checkable: true },
{ key: "0-0-1", title: "Disabled Child", disabled: true }
]
},
{
key: "0-1",
title: "Selectable Disabled",
disabled: true
},
{
key: "0-2",
title: "Not Selectable",
selectable: false
}
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
checkable: true,
selectable: true,
default_expand_keys: ["0-0"],
default_selected_keys: ["0-0-0"],
default_checked_keys: ["0-0-1"]
) %>
Controlled Tree
Controlled checked keys with parent-child aggregation.
Code
<% nodes = [
{
key: "0-0",
title: "Design System",
children: [
{ key: "0-0-0", title: "Tokens" },
{ key: "0-0-1", title: "Components" },
{ key: "0-0-2", title: "Guidelines" }
]
},
{
key: "0-1",
title: "Product",
children: [
{ key: "0-1-0", title: "Roadmap" },
{ key: "0-1-1", title: "Specs" }
]
}
] %>
<%= render Hakumi::Space::Component.new(direction: :vertical, size: :middle) do %>
<%= render Hakumi::Tree::Component.new(
id: "controlled-tree",
nodes: nodes,
checkable: true,
default_expand_keys: ["0-0"],
default_checked_keys: ["0-0-0"]
) %>
<%= render Hakumi::Typography::Text::Component.new(id: "controlled-tree-output", type: :secondary) do %>
Checked keys: []
<% end %>
<% end %>
<script>
(() => {
const treeEl = document.getElementById("controlled-tree")
const output = document.getElementById("controlled-tree-output")
if (!treeEl || !output) return
let wired = false
const wire = () => {
const api = treeEl.hakumiTree
if (!api) return false
const update = () => {
output.textContent = `Checked keys: ${JSON.stringify(api.getCheckedKeys())}`
}
treeEl.addEventListener("hakumi--tree:check", update)
update()
wired = true
return true
}
if (wire()) return
const interval = setInterval(() => {
if (wired) return
if (wire()) clearInterval(interval)
}, 100)
setTimeout(() => clearInterval(interval), 5000)
})()
</script>
Draggable
Drag tree nodes to reorder or move between parents.
Code
<% nodes = [
{
key: "0-0",
title: "Backlog",
children: [
{ key: "0-0-0", title: "Task A" },
{ key: "0-0-1", title: "Task B" }
]
},
{
key: "0-1",
title: "In Progress",
children: [
{ key: "0-1-0", title: "Task C" }
]
}
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
draggable: true,
default_expand_keys: ["0-0", "0-1"]
) %>
Load Data Asynchronously
Load nodes on expand using the public API.
Code
<% nodes = [
{ key: "0-0", title: "Root", async: true },
{ key: "0-1", title: "Static", children: [
{ key: "0-1-0", title: "Child" }
]
}
] %>
<%= render Hakumi::Tree::Component.new(
id: "async-tree",
nodes: nodes,
default_expand_keys: ["0-1"]
) %>
<script>
(() => {
const treeEl = document.getElementById("async-tree")
if (!treeEl) return
let wired = false
const wire = () => {
const api = treeEl.hakumiTree
if (!api) return false
treeEl.addEventListener("hakumi--tree:load", (event) => {
const { key } = event.detail
setTimeout(() => {
api.addNodes(key, [
{ key: `${key}-0`, title: "Loaded Node 1" },
{ key: `${key}-1`, title: "Loaded Node 2" }
])
api.setLoading(key, false)
}, 600)
})
wired = true
return true
}
if (wire()) return
const interval = setInterval(() => {
if (wired) return
if (wire()) clearInterval(interval)
}, 100)
setTimeout(() => clearInterval(interval), 5000)
})()
</script>
Searchable
Search nodes by title with highlight and auto-expand.
Code
<% nodes = [
{ key: "0-0", title: "Design", children: [
{ key: "0-0-0", title: "Tokens" },
{ key: "0-0-1", title: "Components" }
]
},
{ key: "0-1", title: "Engineering", children: [
{ key: "0-1-0", title: "Rails" },
{ key: "0-1-1", title: "Stimulus" }
]
}
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
searchable: true,
default_expand_keys: ["0-0", "0-1"]
) %>
Tree with Line
Show connecting lines and customize the preset switcher icon.
Code
<% nodes = [
{ key: "0-0", title: "Folders", children: [
{ key: "0-0-0", title: "Design" },
{ key: "0-0-1", title: "Assets" }
]
},
{ key: "0-1", title: "Docs" }
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
show_line: true,
default_expand_keys: ["0-0"],
switcher_icon: :plus_square
) %>
Customize Icon
Customize icons per node.
Code
<% nodes = [
{ key: "0-0", title: "Projects", icon: :folder, children: [
{ key: "0-0-0", title: "Specs", icon: :file },
{ key: "0-0-1", title: "Roadmap", icon: :calendar }
]
},
{ key: "0-1", title: "Settings", icon: :setting }
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
default_expand_keys: ["0-0"]
) %>
Customize Collapse/Expand Icon
Customize collapse/expand icon for tree nodes.
Code
<% nodes = [
{ key: "0-0", title: "Overview", children: [
{ key: "0-0-0", title: "Summary" },
{ key: "0-0-1", title: "Details" }
]
}
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
default_expand_keys: ["0-0"],
switcher_icon: :plus
) %>
Directory Tree
Directory tree with multi-select via ctrl/command.
Code
<% nodes = [
{ key: "0-0", title: "src", children: [
{ key: "0-0-0", title: "components" },
{ key: "0-0-1", title: "controllers" }
]
},
{ key: "0-1", title: "README.md", is_leaf: true }
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
directory: true,
multiple: true,
default_expand_keys: ["0-0"]
) %>
Block Node
Block-level clickable nodes.
Code
<% nodes = [
{ key: "0-0", title: "Full-width node", children: [
{ key: "0-0-0", title: "Child A" },
{ key: "0-0-1", title: "Child B" }
]
}
] %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
block_node: true,
default_expand_keys: ["0-0"]
) %>
Virtual Scroll
Virtual list scrolling via height prop.
Code
<% nodes = (1..20).map do |index|
{
key: "0-#{index}",
title: "Node #{index}",
children: (1..4).map { |child| { key: "0-#{index}-#{child}", title: "Node #{index}.#{child}" } }
}
end %>
<%= render Hakumi::Tree::Component.new(
nodes: nodes,
height: 240,
default_expand_keys: ["0-1"]
) %>
Programmatic Rendering
Render Tree via HakumiComponents.renderComponent().
Code
<%= render Hakumi::Space::Component.new(size: :middle) do %>
<%= render(Hakumi::Button::Component.new(type: :primary, id: "programmatic-tree-btn")) { "Render Tree via API" } %>
<%= render Hakumi::Container::Component.new(id: "programmatic-tree-target", width: :fluid, padded: false, centered: false) %>
<% end %>
<script>
(() => {
const button = document.getElementById("programmatic-tree-btn")
if (!button) return
const wire = () => {
if (!window.HakumiComponents?.renderComponent) return false
button.addEventListener("click", async () => {
await window.HakumiComponents.renderComponent("tree", {
params: {
nodes: JSON.stringify([
{ key: "0-0", title: "Programmatic", children: [
{ key: "0-0-0", title: "Dynamic" }
]
}
]),
default_expand_keys: JSON.stringify(["0-0"])
},
target: "#programmatic-tree-target"
})
})
return true
}
if (wire()) return
const onReady = () => {
if (wire()) {
document.removeEventListener("turbo:load", onReady)
window.removeEventListener("load", onReady)
}
}
document.addEventListener("turbo:load", onReady)
window.addEventListener("load", onReady)
})()
</script>
Tree API
| Prop | Type | Default | Description |
|---|---|---|---|
nodes |
Array of Hashes |
[] |
Tree data with keys, titles, and children. |
checkable |
Boolean |
false |
Show checkboxes. |
selectable |
Boolean |
true |
Allow selecting nodes. |
disabled |
Boolean |
false |
Disable the entire tree. |
default_expand_keys |
Array[String] |
[] |
Keys expanded by default. |
default_selected_keys |
Array[String] |
[] |
Keys selected by default. |
default_checked_keys |
Array[String] |
[] |
Keys checked by default. |
expanded_keys |
Array[String] or nil |
nil |
Controlled expanded keys. |
selected_keys |
Array[String] or nil |
nil |
Controlled selected keys. |
checked_keys |
Array[String] or nil |
nil |
Controlled checked keys. |
show_line |
Boolean |
false |
Show connecting lines between nodes. |
switcher_icon |
String or Symbol |
nil |
Custom switcher icon name. |
icon |
String or Symbol |
nil |
Default node icon. |
directory |
Boolean |
false |
Enable directory tree style. |
multiple |
Boolean |
false |
Allow multi-selection. |
draggable |
Boolean |
false |
Enable drag and drop. |
block_node |
Boolean |
false |
Make nodes block-level clickable. |
searchable |
Boolean |
false |
Show search input. |
search_placeholder |
String |
Search |
Placeholder text for search input. |
height |
Number |
nil |
Max height for virtual scroll container. |
check_strictly |
Boolean |
false |
Disable parent-child checking cascade. |
indent |
Number |
24 |
Indent size in pixels. |
**html_options |
Keyword args |
- |
Extra attributes merged into the wrapper. |
JavaScript API (element.hakumiTree)
| Prop | Type | Default | Description |
|---|---|---|---|
getSelectedKeys() |
Function |
- |
Return selected keys. |
setSelectedKeys(keys) |
Function |
- |
Set selected keys. |
getCheckedKeys() |
Function |
- |
Return checked keys. |
setCheckedKeys(keys) |
Function |
- |
Set checked keys. |
getHalfCheckedKeys() |
Function |
- |
Return half-checked keys. |
getExpandedKeys() |
Function |
- |
Return expanded keys. |
setExpandedKeys(keys) |
Function |
- |
Set expanded keys. |
expandAll() |
Function |
- |
Expand all nodes. |
collapseAll() |
Function |
- |
Collapse all nodes. |
toggleNode(key) |
Function |
- |
Toggle node expansion by key. |
selectNode(key, options) |
Function |
- |
Select a node programmatically. |
checkNode(key, checked) |
Function |
- |
Check/uncheck a node programmatically. |
addNodes(parentKey, nodes) |
Function |
- |
Append child nodes to a parent key. |
setLoading(key, loading) |
Function |
- |
Toggle async loading state. |
Events
| Prop | Type | Default | Description |
|---|---|---|---|
hakumi--tree:select |
CustomEvent |
- |
Triggered when selection changes. |
hakumi--tree:check |
CustomEvent |
- |
Triggered when checked keys change. |
hakumi--tree:expand |
CustomEvent |
- |
Triggered when expanded keys change. |
hakumi--tree:load |
CustomEvent |
- |
Triggered when async node needs data. |
hakumi--tree:dragEnd |
CustomEvent |
- |
Triggered after drag and drop. |