09e5b51f5c3c4b2da423434d3af6b381d9398fb4
tp-tree-nav
tp-tree-nav is a low-level, virtualized tree renderer for Lit. It is intentionally “dumb”: it only renders what it is given and emits events. Parents own all state (expanded/selected/custom), perform mutations, and pass updated data back in. This makes it a flexible foundation for building higher-level trees (e.g., VS Code–style explorer, Git ignore/selected indicators, custom icon trees).
Key ideas
- Multiple roots: pass a
rootsarray; each node may havechildren. - Virtualized list: uses
@lit-labs/virtualizerto render a flat, indented list for large trees. - Events only: emits
node-click,node-context, andnode-action(for chevron toggle and context menu actions). Every event includesoriginalEventso parents can cancel or coordinate. - Opt-in actions: provide
defaultActions(array of action objects), and per-nodeactions; node actions override defaults on the sameactionkey. UsebeforeContextMenu(node, actions)to modify or block the menu. - Custom render:
renderNode(node, meta)lets you override row rendering (e.g., icons per node type, state-based styling). Common states likeexpanded/collapsedare parent-managed vianode.states. - Helper methods (pure):
getNodesWithState,clearState,applyStateIfreturn data; they don’t mutate internal state—parents updateroots. - Default icons:
chevron,folder,fileare available viatp-icon; pass string keys or custom icons.
Usage example
import './tp-tree-nav.js';
const tree = document.querySelector('tp-tree-nav');
const roots = [
{
label: 'Project A',
slug: 'project-a',
states: ['expanded'],
icon: 'folder',
children: [
{ label: 'main.js', slug: 'main-js', icon: 'file', states: [] },
{
label: 'src',
slug: 'src',
icon: 'folder',
states: ['expanded'],
children: [
{ label: 'index.js', slug: 'index-js', icon: 'file', states: [] },
],
},
],
},
];
tree.roots = roots;
tree.defaultActions = [
{ label: 'Rename', action: 'rename', icon: 'pencil' },
{ label: 'Delete', action: 'delete', icon: 'delete' },
];
tree.beforeContextMenu = (node, actions) => {
// Example: hide delete on roots
if (node.slug === 'project-a') {
return actions.filter((a) => a.action !== 'delete');
}
return actions;
};
tree.renderNode = (node, { depth, states, path, hasChildren }) => {
const selected = states.includes('selected');
return html`
<div
class="row ${selected ? 'selected' : ''}"
style="--depth: ${depth}"
@click=${(e) => tree.dispatchEvent(new CustomEvent('node-click', { detail: { node, path, originalEvent: e }, bubbles: true, composed: true, cancelable: true }))}
>
<div class="indent"></div>
${hasChildren
? html`<tp-icon class="icon" .icon=${TpTreeNav.chevron}></tp-icon>`
: html`<span class="icon" aria-hidden="true"></span>`}
<tp-icon class="icon" .icon=${tree._resolveIcon(node.icon)}></tp-icon>
<div class="label">${node.label}</div>
</div>
`;
};
// Listen to actions (toggle, context menu items, etc.)
tree.addEventListener('node-action', (e) => {
const { action, node, path } = e.detail;
if (action === 'toggle') {
// Parent mutates data: flip expanded state and reassign roots
const next = tree.applyStateIf('expanded', (n, p) => p.join('/') === path.join('/'));
tree.roots = next;
}
});
Building higher-level trees
Wrap tp-tree-nav in a specialized component (e.g., file explorer) that:
- Tracks expansion/selection state on nodes.
- Supplies icons per node type via
renderNodeoriconstrings. - Defines default/context actions relevant to the domain.
- Handles action events to mutate the source data and pass updated
rootsback.
Description
Languages
JavaScript
100%