e7b858a5113fef4e7713b0ccf321ab8ade587fda
tp-tree-nav
tp-tree-nav is a low-level, virtualized tree renderer for Lit. It renders what it is given and emits events; parents own state. You can wrap it directly or extend it to add opinionated behavior (selection, expansion, inline actions, empty states, etc.).
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, inline actions, and context menu actions). Each event carriesoriginalEventso parents can cancel or coordinate. - Opt-in actions: provide
defaultActionsand 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 (icons per node type, state-based styling). Common states likeexpanded/selectedare parent-managed vianode.statesor managed mode. - Helpers (pure):
getNodesWithState,clearState,applyStateIfreturn new data; parents reassignroots. - Default icons:
chevron,folder,fileare available viatp-icon; pass string keys or custom icons.
Quick example (wrap directly)
import './tp-tree-nav.js';
const tree = document.querySelector('tp-tree-nav');
tree.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.defaultActions = [
{ label: 'Rename', action: 'rename', icon: 'pencil' },
{ label: 'Delete', action: 'delete', icon: 'delete' },
];
tree.beforeContextMenu = (node, actions) =>
node.slug === 'project-a' ? actions.filter((a) => a.action !== 'delete') : actions;
// simple toggle handler
const targetPath = (path) => path.join('/');
tree.addEventListener('node-action', (e) => {
if (e.detail.action !== 'toggle') return;
const target = targetPath(e.detail.path);
tree.roots = tree.applyStateIf('expanded', (_n, p) => targetPath(p) === target);
});
Two ways to use it
A) Wrap directly (keep tp-tree-nav “dumb”)
- Manage all state yourself; set
rootsand optionaldefaultActions/actions. - Listen to
node-action(chevron/double-click toggles, inline actions, context actions) andnode-click, mutate your data, then reassignroots. - Use
renderNodefor custom rows;renderEmptyfor empty states. - Use helpers (
applyStateIf,clearState,getNodesWithState) for immutable transforms/queries.
B) Extend tp-tree-nav (add opinionated behavior)
Leverage managed mode and hooks in a subclass:
import { TpTreeNav } from './tp-tree-nav.js';
class MyTree extends TpTreeNav {
constructor() {
super();
this.manageState = true; // base tracks expand/select
this.autoExpandNew = false; // set true to auto-expand unseen paths
this.selectionState = 'selected'; // state name for selection
this.expandOnDoubleClick = true; // toggle expand via double-click
this.multiSelect = false; // enable multi-select if needed
this.showActions = true; // render inline actions when present
this.renderEmpty = () => html`<div class="empty">No items</div>`;
}
set data(items) {
this.items = items; // raw items with slug/label/icon/children/actions
}
_renderRow(item, meta) {
const { node } = item;
const selected = meta.states.includes(this.selectionState);
return html`
<div
class="row ${selected ? 'selected' : ''}"
style="--depth:${meta.depth}"
@click=${(e) => this._onRowClick(item, e)}
>
<div class="indent"></div>
<div
class="chevron-btn ${meta.expanded ? 'expanded' : ''}"
?hidden=${!meta.hasChildren}
@click=${(e) => this._onChevronClick(item, e)}
>
<tp-icon class="icon" .icon=${TpTreeNav.chevron}></tp-icon>
</div>
<tp-icon class="icon" .icon=${this._resolveIcon(node.icon)}></tp-icon>
<div class="label">${node.label}</div>
${node.actions?.length
? html`<div class="actions">${node.actions.map((action) => html`
<tp-icon
class="action-icon"
.icon=${this._resolveIcon(action.icon)}
@click=${(e) => this._onInlineAction(item, action, e)}
></tp-icon>
`)}</div>`
: null}
</div>
`;
}
}
customElements.define('my-tree', MyTree);
Key props when extending
items: raw data (slug,label,icon,children, optionalactions).manageState: base manages expand/select when true.expandedPaths/selectedPaths: arrays or Sets to seed/restore state.selectionState: state name for selection (default'selected').multiSelect: allow multiple selections.expandOnDoubleClick: toggle expansion on double-click (uses click detail).autoExpandNew: auto-expand unseen nodes when first seen (default false).applyStates:(node, pathParts, states) => extraStates[]to add custom states.applySelection: override selection logic(node, pathParts, states, originalEvent) => nextStates.applyToggle: override expand/collapse logic(node, pathParts, states, originalEvent) => nextStates.showActions: render inline actions if provided.renderEmpty: custom empty renderer.
Events (both modes)
node-click: emitted on row click.node-action: emitted for toggles (action: 'toggle', source chevron/double-click), inline actions, and context menu actions.node-context: before showing context menu;preventDefaultto cancel.node-drop: emitted on valid drop (detail: { source, target, position }).
Helpers (both modes)
TpTreeNav.buildTree(items, { expandedPaths, selectedPaths, selectionState, applyStates, knownPaths, autoExpandNew })— pure helper used by managed mode; can be used externally.applyStateIf(state, predicate),clearState(state),getNodesWithState(state)for immutable transforms/queries.
Drag and Drop
Enable drag and drop by setting allowDragAndDrop = true.
- Validation: Provide
canDrop(source, target, position)to allow/deny drops.positionis'inside','before', or'after'. - Event: Listen to
node-dropevent (detail: { source, target, position, originalEvent }).
Data shape
slug(unique per sibling),label,icon(string key or icon data),children(array), optionalactions({ action, label?, icon?, tooltip? }).
Description
Languages
JavaScript
100%