File Tree — a template that calls itself
A directory contains directories. Rendering one means rendering its children
the same way, to whatever depth the data happens to go — and you don't know
that depth when you write the template.
Go's html/template handles this natively: a {{define}} block can invoke
itself. LiveTemplate could not, until v0.19.0, and the reason is worth
understanding because it explains what changed. The full source is
examples/file-tree/.
Try it
File tree
A directory contains directories, so the template that renders one
invokes itself for each child. Expand a folder or star a file
and only that node's markup is sent over the wire.
Expand a folder or star a file. Every control above is a plain
<button name="..."> inside a form — no custom attributes, no hand-written
JavaScript.
The template
The whole recipe is one self-referential block. node renders an entry, and
for a directory it loops over the children invoking node again:
{{define "node"}}
<li data-key="{{.Path}}">
{{if .IsDir}}
<form method="POST" style="display:inline">
<button name="toggle" value="{{.Path}}">{{if .Expanded}}▾{{else}}▸{{end}} {{.Name}}/</button>
</form>
{{if .Expanded}}
<ul>
{{range .Children}}{{template "node" .}}{{end}}
</ul>
{{end}}
{{else}}
<span class="file">
<form method="POST" style="display:inline">
<button name="star" value="{{.Path}}">{{if .Starred}}★{{else}}☆{{end}}</button>
</form>
{{.Name}}
</span>
{{end}}
</li>
{{end}}
Why this used to fail
LiveTemplate normally inlines {{template}} calls when it parses: it
splices the invoked body into the caller, producing one flat template it can
analyze for statics and dynamics. A self-referential call has no fixed point
to inline toward — expanding node yields another node to expand, forever.
So recursion was rejected at parse time.
From v0.19.0, the parser first finds templates reachable from themselves and
leaves those calls un-inlined, evaluating them at build time instead. The
recursive region becomes nested tree nodes rather than flattened markup, which
is what keeps it inside the reactive tree instead of degrading to opaque HTML.
Why the update stays small
Because the recursion produces real nested structure, a change deep in the
tree diffs against that structure. Starring query.go four levels down sends
an update addressed to that one node — not a re-render of /internal, and not
a re-send of the branch containing it. The sibling migrate.go isn't just
visually unchanged; its DOM node is never replaced.
That property is what makes deep trees practical. Before per-leaf diffing, a
change anywhere under a branch meant re-sending the branch, which on a
five-level tree is roughly a hundred times more bytes than the change itself.
Keys matter here
Each <li> carries data-key="{{.Path}}", and path — not name — is the right
choice. Sibling names repeat across directories: two README.md files in
different folders are different nodes. Keying on name would let the diff
engine confuse them and move the wrong row; a full path cannot collide.
Depth is capped
Recursion runs until the data stops nesting, so data that refers to itself
would recurse forever. LiveTemplate caps invocation depth at 128 by
default, surfacing an error rather than overflowing the stack. Raise it with
WithMaxTemplateDepth(n) or LVT_MAX_TEMPLATE_DEPTH only when your data is
legitimately deeper — see the
template support matrix
for the full behavior, including why a too-low cap can go unnoticed on first
render.