TanstackTable¶
The TanstackTable panel is an accessible tree and treegrid widget built on TanStack Table, wrapped in a Vue.js bridge. It renders a plain list of dicts as a tree, or as a tree plus columns, with a toolbar, drag and drop, inline editing, undo and a full ARIA treegrid.
The one idea: data flows one way¶
Python owns source. The browser never writes it.
The user does something. The browser emits an intent (
move,add,rename,edit,delete,cut,copy,paste,transfer,drop_files,undo,redo,lazy_load) through_event_data.Python validates it, asks your callbacks, and rewrites
source.The new tree is pushed back down.
So every rule about how the tree reshapes lives in Python, is testable without a browser, and cannot be bypassed by a hand-built event.
Basic usage¶
Nodes are plain dicts. key and title are the only required fields; children nests them.
from panelini.panels.tanstack.table import TanstackTable
source = [
{
"key": "docs",
"title": "Documents",
"children": [
{"key": "report", "title": "report.pdf", "allow_children": False},
{"key": "notes", "title": "notes.md", "allow_children": False},
],
},
{"key": "config", "title": "config.yaml", "allow_children": False},
]
table = TanstackTable(source=source, options={"expand_all": True})
Add columns to switch from tree-only mode to treegrid mode.
Node fields¶
Field |
Meaning |
|---|---|
|
Unique id. Everything else refers to a node by it. |
|
The label in the tree column. |
|
Child nodes. |
|
Names an entry of |
|
|
|
A CSS class for the row. |
|
Names an entry of |
|
|
anything else |
Column data, read by the column’s |
Parameters¶
Direction is P>B (Python to browser), B>P (browser to Python), both, or Python only for a value the browser never reads.
Parameter |
Type |
Direction |
What it is |
|---|---|---|---|
|
list |
Python only |
The tree. Python’s to write; never sent as-is (a derived view crosses instead). |
|
list |
P>B |
Column definitions. Empty means tree-only mode. |
|
dict |
P>B |
Display and behaviour options, see below. |
|
dict |
P>B |
Extra |
|
dict |
P>B |
|
|
str |
both |
Search text. Hides rows that neither match nor lead to a match. |
|
str |
both |
Row the inline editor is open on, |
|
str |
both |
Column it is open on, |
|
list |
both |
|
|
dict |
both |
|
|
list |
both |
Keys of the expanded nodes. |
|
list |
both |
Keys of the selected nodes. |
|
int |
Python only |
How many tree states to keep, default 20, |
|
bool |
P>B |
Whether a step is available. Drives the toolbar buttons. |
|
dict |
P>B |
|
Sorting, resizing and filtering are view state: they never touch source, so nothing about them is recorded for undo.
Columns¶
A column def is a dict.
Key |
Default |
What it does |
|---|---|---|
|
required |
Column id. Also the node field, unless |
|
|
Header label. |
|
|
Node field this column reads and writes. |
|
|
Starting width in pixels. |
|
|
Bounds a resize drag may not cross. |
|
|
|
|
|
|
|
|
|
|
|
|
|
- |
What a |
|
- |
For a |
The first column is the tree column, whatever it declares: it carries the indent, the twisty, the icon and the title, and it is renamed rather than edited.
Options¶
Key |
Default |
What it does |
|---|---|---|
|
|
Names the treegrid for assistive technology. |
|
|
Indent per tree level, in pixels. |
|
|
Keep every branch open, including branches that arrive later. |
|
|
Turn drag and drop on. |
|
|
|
|
|
|
|
|
A click on the only selected row clears the selection. |
|
absent |
Ordered list of action ids, or |
|
absent |
The same ids, as a right-click context menu. |
|
absent |
The same ids again, as buttons at the trailing edge of every row. |
|
|
Accessible names for those three. |
|
|
|
|
|
Branches above leaves at every level, whichever way a column is sorted. |
|
|
|
|
off |
|
|
- |
Two tables naming the same group accept each other’s dragged rows. |
|
|
Prefix for keys minted for new nodes. |
|
- |
Extra |
|
|
|
|
|
|
|
|
Extensions and MIME patterns: |
|
|
Cap on one dropped file. |
|
- |
Template a dropped file’s node is minted from. |
drop_accept and drop_max_bytes are decided in Python. The browser reads them only to skip loading the bytes of a file that was going to be refused.
Editing¶
Set editable: True on a column and pick an editor.
|
Control |
Commits |
|---|---|---|
|
text box |
|
|
number input with |
same, after Python checks the range |
|
checkbox |
the moment you tick it |
|
dropdown of |
the moment you choose |
Double click a cell to edit it. On a focused row,
Enteropens the first editable cell (and activates the row instead when the table has none), whileF2is the tree column’s rename.TabandShift+Tabwalk the row’s editable cells;Escapeleaves without writing.A value the column cannot hold, or one
action_callbackrefuses, reopens the editor holding what was typed, markedaria-invalid, so it is corrected rather than retyped.The value lands on the node itself, never on the
typeit names.The tree column is the exception: it is a
renameintent, which carries the file-type warning and the icon rule.
Callbacks and vetoes¶
All five are constructor arguments.
Callback |
Signature |
Sees |
Returning |
|---|---|---|---|
|
|
every drag, |
cancels that node’s move |
|
|
|
leaves |
|
|
every event, after Python has applied it |
nothing; this one only reports |
|
|
a cross-pane drag whose partner is not a |
falls back to the ordinary path |
|
|
the first expand of a node marked |
not a veto: return the child list, or |
positionisbefore,afterorchild.undoandredoare never asked: they replay states already allowed.A cross-pane
transferasksaction_callbackon the table the nodes leave, andmove_callbackon the table they arrive in.The thirteen intents come back carrying
applied, soevent_callbackcan tell what landed. Any other event is forwarded untouched,activateamong them, which arrives withkeyalone.
def allow_move(key, anchor_key, position):
return not (position == "child" and anchor_key == "archive")
def allow_action(action, params):
return action != "delete"
table = TanstackTable(
source=source,
move_callback=allow_move,
action_callback=allow_action,
event_callback=lambda name, params: print(name, params.get("applied")),
)
Public API¶
Forty methods, grouped by what they touch. A method that reshapes the tree rewrites source and records an undo step, so an application’s change is undoable exactly like a user’s. Readers change nothing, selection, expansion, sort and width are view state, set_children fills a lazy branch, and set_source clears the history outright, so none of those are recorded.
Group |
Methods |
|---|---|
Tree edits |
|
Selection |
|
Expansion |
|
Clipboard |
|
History |
|
Sort and width |
|
Types |
|
Lazy loading |
|
Events |
|
batch() is a context manager: everything inside it becomes one push and one undo step. A block of set_children calls is one push and no undo step, since a lazy branch arriving is not an edit.
with table.batch():
for path in paths:
table.add_node({"key": path, "title": path}, parent_key="docs")
Pure tree helpers live beside the panel in panelini.panels.tanstack.table.tree and import neither Panel nor param: iter_nodes, find_node, find_parent, is_descendant, subtree_keys, new_key and the rest.
Accessibility¶
This is why the panel exists.
A real ARIA
treegrid:rowgroup,row,columnheader,gridcell, plustoolbar,menu/menuitemandalertdialogfor the rename confirmation.20
aria-*attributes, includingaria-level,aria-posinset,aria-setsize,aria-expanded,aria-selected,aria-rowindex,aria-colindex,aria-sort,aria-busy(a lazy branch loading),aria-invalid(a refused edit) andaria-keyshortcuts.Every structural action has a key, listed in the action table above. Arrow keys,
HomeandEndnavigate,EnterandSpaceactivate and select, and the context menu opens onShift+F10or the menu key rather than only on a right click.A roving
tabindex, soTabstays the way out of the grid.Ctrlcombinations are taken only while focus is inside the grid, soCtrl+Fdoes not steal the browser’s own find on the rest of the page.
Large trees¶
Two separate costs, two separate answers.
Cost |
Answer |
Effect |
|---|---|---|
Render |
a windowed rowgroup |
only the visible rows plus a small overscan are in the DOM, however far you scroll |
Wire |
|
only opened branches cross; a pruned branch arrives as a twisty and is filled on expand |
Pruning takes a ten thousand node tree from about 950 kB on the wire to about 6 kB when that tree is a hundred folders of a hundred files. The ratio follows the shape: the wider the fan-out, the more a collapsed view leaves behind, and the big tree example’s thousand folders of ten measures about ten times rather than a hundred and fifty. It is off by default, because it also means a search reaches unloaded branches through Python rather than through the browser alone.
Two things to know before turning it on. A branch that has crossed stays with the browser when it is collapsed again, which is what makes re-opening it instant, so the wire cost of a session only ever grows. And the toolbar’s expand-all opens the rows the browser holds, so it cannot open a branch that has not crossed: call expand_all() from Python instead, which reads the tree Python owns and sends what the browser is missing.
Mark a branch lazy: True and answer lazy_callback to build a tree that never fully exists in memory at all. See Filesystem browser - lazy loading from a real backend.
Examples¶
Treegrid - columns and cell editors - five columns, sorting, resizing, node types, all four cell editors
VFS explorer - two panes and external file drop - two panes, cross-pane drag, toolbar, context menu, files from the desktop
Big tree - one node to a million, and what each size costs - one node to a million, with the wire cost of each read out live
Filesystem browser - lazy loading from a real backend - a real filesystem loaded one directory at a time
API Reference¶
See the full API documentation: panelini.panels.tanstack.table.table.TanstackTable