TreeView#
TreeView renders hierarchical data with expand/collapse toggles, indentation, and virtualization: only visible rows are rendered. Maps to UI Toolkit's TreeView.
Import#
import { TreeView } from "onejs-react"Basic Usage#
Data is a plain nested array. Each node has data (whatever you want), optional children, and an optional numeric id. renderItem returns the JSX for one row and receives the node's data directly:
const project = [
{
id: 1, data: "Assets", children: [
{ id: 2, data: "Player.cs" },
{ id: 3, data: "Enemy.cs" },
],
},
{ id: 4, data: "README.md" },
]
function FileTree() {
return (
<TreeView
rootItems={project}
fixedItemHeight={22}
autoExpand
renderItem={(name: string) => <Label text={name} style={{ flexGrow: 1 }} />}
style={{ height: 300 }}
/>
)
}The toggle and the indentation come from UI Toolkit, so renderItem returns only the row's content. A richer row is just more JSX:
renderItem={(node: FileNode) => (
<View style={{ flexDirection: "row", alignItems: "center", flexGrow: 1 }}>
<View style={{ width: 12, height: 12, borderRadius: 3, marginRight: 8, backgroundColor: node.color }} />
<Label text={node.name} style={{ fontSize: 14, flexGrow: 1 }} />
<Label text={node.meta} style={{ fontSize: 11, color: "#8b94a6" }} />
</View>
)}renderItem needs onejs-react 0.1.56 or newer.
Ids and Updating Data#
Ids identify nodes across data updates: selection and expansion state are tracked by id. They are optional (omitted ids are auto-assigned), but provide stable ids whenever the tree's data changes over time, or state will not carry over.
To update the tree, pass a new rootItems reference. Mutating the existing array does nothing, the same rule as React state in general:
const [items, setItems] = useState(initialItems)
// Adds a node: new arrays along the changed path
setItems([...items, { id: 99, data: "New.txt" }])Props#
Required Props#
| Prop | Type | Description |
|---|---|---|
rootItems | TreeViewItem[] | Nested data: { id?, data, children? } |
renderItem | (data, index) => ReactNode | JSX for one row. Use this, or the imperative pair below, not both |
Row Props (imperative alternative)#
| Prop | Type | Description |
|---|---|---|
makeItem | () => VisualElement | Factory for row content elements |
bindItem | (element, index, data) => void | Populate a row; data is the node's data |
unbindItem | (element, index, data) => void | Called when a row is recycled |
destroyItem | (element) => void | Called when a row element is retired |
Optional Props#
| Prop | Type | Default | Description |
|---|---|---|---|
fixedItemHeight | number | Fixed row height (fastest) | |
virtualizationMethod | "FixedHeight", "DynamicHeight" | "FixedHeight" | Virtualization strategy |
autoExpand | boolean | false | Expand all items when data is set |
selectionType | "None", "Single", "Multiple" | "Single" | Selection mode |
onSelectionChange | (items, ids) => void | Selected node data + ids | |
showBorder | boolean | false | Border around the tree |
showAlternatingRowBackgrounds | "None", "ContentOnly", "All" | "None" | Zebra striping |
Rows recycle as the tree scrolls, exactly as ListView's do, and renderItem keeps that while letting React own what is inside a row. How Rows Work on the ListView page covers what recycling means for state inside a row; it reads the same here.
Selection#
onSelectionChange hands you the selected nodes' data and their ids:
function SelectableTree({ items }) {
const [current, setCurrent] = useState(null)
return (
<View>
<Label text={current ? `Selected: ${current.name}` : "Nothing selected"} />
<TreeView
rootItems={items}
fixedItemHeight={24}
selectionType="Single"
onSelectionChange={(datas, ids) => setCurrent(datas[0] ?? null)}
renderItem={(node: FileNode) => <Label text={node.name} style={{ flexGrow: 1 }} />}
style={{ height: 400 }}
/>
</View>
)
}Expand, Collapse, and Selection by Id#
The underlying UI Toolkit methods are available through a ref:
function Controls() {
const tree = useRef(null)
return (
<View>
<View style={{ flexDirection: "row" }}>
<Button text="Expand All" onClick={() => tree.current.ExpandAll()} />
<Button text="Collapse All" onClick={() => tree.current.CollapseAll()} />
<Button text="Jump" onClick={() => tree.current.SetSelectionById(3)} />
</View>
<TreeView ref={tree} rootItems={items} ... />
</View>
)
}Useful methods: ExpandAll(), CollapseAll(), ExpandItem(id), CollapseItem(id), IsExpanded(id), SetSelectionById(id), GetTreeCount().
Imperative Rows#
makeItem and bindItem are the older API, and still the right one when a row is a single element whose text you want to set with no React in the path. The element is recycled, so bindItem must set every property it cares about:
<TreeView
rootItems={project}
fixedItemHeight={22}
makeItem={() => {
const label = new CS.UnityEngine.UIElements.Label()
label.style.flexGrow = 1
return label
}}
bindItem={(element, index, data) => {
element.text = data
}}
/>These are raw C# elements rather than React ones, so styles want real structs (new CS.UnityEngine.Color(...), not a colour string), and children are reached by position with element.ElementAt(i). Q(name) also works after a one-time useExtensions(CS.UnityEngine.UIElements.UQueryExtensions) (OneJS 3.1.3+). See ListView: Imperative Rows for the same notes in full.
Pass renderItem or the imperative pair, never both. TypeScript rejects a TreeView that has both.
Styling the Built-in Chrome#
The expand toggle, indentation, and row highlights come from UI Toolkit's default theme, which assumes a light-friendly palette (the default hover is a light grey that fights light text on dark UIs). Restyle them with a small stylesheet scoped under a class on your TreeView:
compileStyleSheet(`
.my-tree .unity-tree-view__item-toggle {
margin-right: 6px;
}
.my-tree .unity-tree-view__item-toggle .unity-toggle__checkmark {
background-color: rgba(0, 0, 0, 0);
-unity-background-image-tint-color: #8b93a7;
}
.my-tree .unity-collection-view__item:hover {
background-color: rgba(91, 156, 248, 0.12);
}
.my-tree .unity-collection-view__item--selected,
.my-tree .unity-collection-view__item--selected:hover {
background-color: #2f4a7a;
}
`, "my-tree")<TreeView className="my-tree" ... />Rows are .unity-collection-view__item, with a --selected modifier and a :hover state; the expand toggle is .unity-tree-view__item-toggle, and the arrow inside it is .unity-toggle__checkmark (clear its background for a bare chevron). ListView rows share the same unity-collection-view__item classes, so the row rules work there too.
TreeView or ListView?#
Use TreeView when the data is hierarchical and users need to fold branches. For flat data, ListView is simpler and slightly cheaper. Both virtualize, so either handles thousands of rows.