Table tree cell
The table tree cell renders a tree branch, a round marker and its content in a single cell. Place it instead of a Cell in the column that should show the hierarchy, and give that column a (usually empty) Header. It draws the guide lines automatically from the level of the surrounding tree cells, so all you provide is the depth and whether a node is expandable.
Expansion stays in your app
The tree cell holds no expansion state. It only draws the marker and emits toggle when an expandable marker is clicked. Keep your own expanded/collapsed state, flatten your tree into the rows you want to render, and pass each node its level and is-expanded. Collapsing a node simply means rendering fewer rows.
WARNING
This component is best used within a Row, as the first cell of a column that has a matching Header.
Required icons
Props
level: number
The depth of this node, starting at `0` for a root. The table derives the guide lines from the levels of the visible tree cells around it.
color?: FluxColor | string
The colour of the marker. Accepts a `FluxColor` or any custom CSS colour string. Defaults to gray, with no distinction between expandable and leaf markers.
Default: gray
is-expandable?: boolean
Renders the marker as a toggle with a chevron. Leave it off to render a plain leaf marker.
is-expanded?: boolean
Whether the node is currently expanded. Controls the chevron direction only; your application decides which child rows are rendered.
Emits
toggle: []
Triggered when the marker of an expandable node is clicked. Expansion state is owned by your application, so handle this by flipping your own expanded state and re-rendering the visible rows.
Slots
default
The content of the cell, shown after the marker.
Examples
File tree
A collapsible file tree whose expanded state lives in the page, driving which rows render.
<template>
<FluxPane>
<FluxTable is-hoverable>
<template #header>
<FluxTableHeader :min-width="270"/>
<FluxTableHeader is-shrinking>Kind</FluxTableHeader>
<FluxTableHeader
is-shrinking
is-numeric>Size</FluxTableHeader>
</template>
<FluxTableRow
v-for="row in rows"
:key="row.id">
<FluxTableTreeCell
:level="row.level"
:is-expandable="row.hasChildren"
:is-expanded="!collapsed.has(row.id)"
@toggle="toggle(row.id)">
{{ row.name }}
</FluxTableTreeCell>
<FluxTableCell>{{ row.hasChildren ? 'Folder' : 'File' }}</FluxTableCell>
<FluxTableCell is-numeric>{{ row.size ?? '—' }}</FluxTableCell>
</FluxTableRow>
</FluxTable>
</FluxPane>
</template>
<script
setup
lang="ts">
import { FluxPane, FluxTable, FluxTableCell, FluxTableHeader, FluxTableRow, FluxTableTreeCell } from '@flux-ui/components';
import { computed, reactive } from 'vue';
type Node = {
readonly id: string;
readonly name: string;
readonly size?: string;
readonly children?: Node[];
};
type Row = {
readonly id: string;
readonly name: string;
readonly size?: string;
readonly level: number;
readonly hasChildren: boolean;
};
const tree: Node[] = [
{
id: 'src',
name: 'src',
children: [
{
id: 'components',
name: 'components',
children: [
{id: 'button', name: 'FluxButton.vue', size: '4 KB'},
{id: 'table', name: 'FluxTable.vue', size: '12 KB'}
]
},
{id: 'index', name: 'index.ts', size: '1 KB'}
]
},
{id: 'package', name: 'package.json', size: '2 KB'}
];
const collapsed = reactive(new Set<string>());
const rows = computed<Row[]>(() => {
const flat: Row[] = [];
const walk = (nodes: Node[], level: number): void => {
for (const node of nodes) {
const hasChildren = !!node.children?.length;
flat.push({id: node.id, name: node.name, size: node.size, level, hasChildren});
if (hasChildren && !collapsed.has(node.id)) {
walk(node.children!, level + 1);
}
}
};
walk(tree, 0);
return flat;
});
function toggle(id: string): void {
if (collapsed.has(id)) {
collapsed.delete(id);
} else {
collapsed.add(id);
}
}
</script>Coloured markers
The same tree with a color per level, mixing FluxColor names with your own palette.
<template>
<FluxPane>
<FluxTable is-hoverable>
<template #header>
<FluxTableHeader :min-width="270"/>
<FluxTableHeader is-shrinking>Members</FluxTableHeader>
</template>
<FluxTableRow
v-for="row in rows"
:key="row.id">
<FluxTableTreeCell
:level="row.level"
:color="levelColors[row.level] ?? 'gray'"
:is-expandable="row.hasChildren"
:is-expanded="!collapsed.has(row.id)"
@toggle="toggle(row.id)">
{{ row.name }}
</FluxTableTreeCell>
<FluxTableCell>{{ row.members ?? '—' }}</FluxTableCell>
</FluxTableRow>
</FluxTable>
</FluxPane>
</template>
<script
setup
lang="ts">
import { FluxPane, FluxTable, FluxTableCell, FluxTableHeader, FluxTableRow, FluxTableTreeCell } from '@flux-ui/components';
import type { FluxColor } from '@flux-ui/types';
import { computed, reactive } from 'vue';
type Node = {
readonly id: string;
readonly name: string;
readonly members?: number;
readonly children?: Node[];
};
type Row = {
readonly id: string;
readonly name: string;
readonly members?: number;
readonly level: number;
readonly hasChildren: boolean;
};
const levelColors: FluxColor[] = ['danger', 'warning', 'info'];
const tree: Node[] = [
{
id: 'west',
name: 'Pool West',
children: [
{
id: 'laurentius',
name: 'Laurentius',
children: [
{id: 'bernade', name: 'Bernade', members: 12},
{id: 'cornelis', name: 'Cornelis', members: 8}
]
},
{id: 'reger', name: 'De Reger', members: 5}
]
},
{id: 'east', name: 'Pool East', members: 21}
];
const collapsed = reactive(new Set<string>());
const rows = computed<Row[]>(() => {
const flat: Row[] = [];
const walk = (nodes: Node[], level: number): void => {
for (const node of nodes) {
const hasChildren = !!node.children?.length;
flat.push({id: node.id, name: node.name, members: node.members, level, hasChildren});
if (hasChildren && !collapsed.has(node.id)) {
walk(node.children!, level + 1);
}
}
};
walk(tree, 0);
return flat;
});
function toggle(id: string): void {
if (collapsed.has(id)) {
collapsed.delete(id);
} else {
collapsed.add(id);
}
}
</script>Custom colours
Passing arbitrary CSS colour strings, so each node carries its own hex value.
<template>
<FluxPane>
<FluxTable is-hoverable>
<template #header>
<FluxTableHeader :min-width="270"/>
<FluxTableHeader is-shrinking>Owner</FluxTableHeader>
</template>
<FluxTableRow
v-for="row in rows"
:key="row.id">
<FluxTableTreeCell
:level="row.level"
:color="row.color"
:is-expandable="row.hasChildren"
:is-expanded="!collapsed.has(row.id)"
@toggle="toggle(row.id)">
{{ row.name }}
</FluxTableTreeCell>
<FluxTableCell>{{ row.owner ?? '—' }}</FluxTableCell>
</FluxTableRow>
</FluxTable>
</FluxPane>
</template>
<script
setup
lang="ts">
import { FluxPane, FluxTable, FluxTableCell, FluxTableHeader, FluxTableRow, FluxTableTreeCell } from '@flux-ui/components';
import { computed, reactive } from 'vue';
type Node = {
readonly id: string;
readonly name: string;
readonly color: string;
readonly owner?: string;
readonly children?: Node[];
};
type Row = {
readonly id: string;
readonly name: string;
readonly color: string;
readonly owner?: string;
readonly level: number;
readonly hasChildren: boolean;
};
const tree: Node[] = [
{
id: 'design',
name: 'Design system',
color: '#8b5cf6',
children: [
{
id: 'tokens',
name: 'Tokens',
color: '#ec4899',
children: [
{id: 'color', name: 'Colour', color: '#f97316', owner: 'Anouk'},
{id: 'type', name: 'Typography', color: '#f97316', owner: 'Milan'}
]
},
{id: 'icons', name: 'Icons', color: '#ec4899', owner: 'Sara'}
]
},
{id: 'brand', name: 'Brand guidelines', color: '#8b5cf6', owner: 'Team'}
];
const collapsed = reactive(new Set<string>());
const rows = computed<Row[]>(() => {
const flat: Row[] = [];
const walk = (nodes: Node[], level: number): void => {
for (const node of nodes) {
const hasChildren = !!node.children?.length;
flat.push({id: node.id, name: node.name, color: node.color, owner: node.owner, level, hasChildren});
if (hasChildren && !collapsed.has(node.id)) {
walk(node.children!, level + 1);
}
}
};
walk(tree, 0);
return flat;
});
function toggle(id: string): void {
if (collapsed.has(id)) {
collapsed.delete(id);
} else {
collapsed.add(id);
}
}
</script>