6 Interactive and explorable documents
Documents a reader can operate: reactive js cells, inputs without boilerplate, and 3-D.
Taliesin runs {js} cells in the browser and wires them into a small reactive
graph. Inputs are the only declarative convenience on top of that graph.
Everything here is client-side and offline, built from the same block model as
the rest of the page. The preview stays read-only: a reader interacts with the
output, never the source.
The fenced examples below are source listings; the {{< input >}} section runs live
on this page.
6.1 Reactive {js} cells
A {js} cell declares its place in a dependency graph with leading //|
directives (they must come before any code):
//| viewof: NAMEmakes the cell an input control published underNAME.//| name: NAMEpublishes the cell’s return value underNAME.//| input: A, Bconsumes those names; the cell re-runs when any of them change.
Inside a cell, read values through the injected tali API: tali.value("k") returns
a control’s current value, and tali.get("squared") returns the last value a
//| name cell published. A first cell publishes a slider as n:
//| viewof: n
const input = document.;
input. = "range"; input. = "1"; input. = "12"; input. = "3";
return input;
A second derives a value from it, and a third consumes that derived value, so the
chain n to squared to the readout stays in sync transitively:
//| name: squared
//| input: n
return tali. ** 2;
//| input: squared
return document.;
When an input changes, Taliesin re-runs exactly the cells downstream of it, once each,
in dependency order, following //| name edges; a cycle is reported as an error rather
than run. Plot and d3 are available as globals for drawing, and a cell either
returns a DOM node (mounted for you) or builds into tali.container.
The node a cell returns is mounted after the cell body finishes, so a guard like
if (!node.isConnected) return; inside the body always fires and the cell never paints.
An init gated on offsetWidth or getBoundingClientRect() is the same mistake.
Use invalidation instead: it is a promise resolving when the cell is about to
re-run or leave the page, whether or not the node ever reached the DOM, so it is where
you cancel animation frames and dispose GPU resources. If you need post-mount
measurements, take them inside a requestAnimationFrame callback.
A viewof input doesn’t need to flow through a //| name cell first: a consumer can
list it directly in //| input:.
//| input: n
return document.;
6.2 Inputs without boilerplate: {{< input >}}
The {{< input >}} shortcode emits a labelled, keyboard-accessible control whose
value becomes a reactive node, so a {js} cell reacts with //| input: and no
control-building code. Five of them are ordinary form fields:
| Argument | Required | Default | Effect |
|---|---|---|---|
name= | warns if omitted | (none) | The reactive node’s name (what a consuming cell reads). Omitting it warns (the control still renders, but it can’t feed the reactive graph). |
type= | no | slider | One of slider, number, checkbox, text, select. An unknown type warns with a did-you-mean. |
label= | no | the name | The visible label text. |
value= | no | (none) | The initial value. A checkbox is pre-checked only when value is the literal true; a select’s value must exactly string-match one of the options to pre-select it. |
min= max= step= | no | (none) | Numeric bounds for slider and number. |
options= | for select | (none) | A comma-separated option list. Required for type="select"; omitting it warns. |
tali.value() returns a number for slider and number, a boolean for
checkbox, and a string for text and select. A consuming cell is then a
{js} cell with //| input: k that reads tali.value("k").
A slider renders as HTML’s <input type="range"> with a live <output> readout
that updates as you drag and sits beside the track.
number shares its numeric min/max/step/value attributes but renders as a spinner
with no readout.
Here is the first shortcode of that block running live, with the consuming cell under it. Drag the slider and the curve redraws.
6.3 Interactive 3-D with a {js} cell
Any node a cell returns is mounted, and that node can be a WebGL canvas, so an
interactive 3-D scene is a cell that imports a 3-D library and returns its renderer’s
canvas. Construct the renderer with alpha: true, or its clear colour paints an
opaque slab over the page in one theme or the other. Cancel animation frames and
dispose GPU resources on invalidation. If you fetch the library over the network
(three.js from a CDN, say), you give up the offline default, so the page needs
http(s), not file://.
6.4 The Python → JS bridge
define() in a Python cell hands values to {js} cells, so you can compute
in Python and render reactively in the browser. Its signature is keyword-only,
define(name=value, ...): each keyword becomes a published name, and each
value is JSON-serialized into the page for the {js} runtime to pick up. A {js}
cell then reads the value as tali.defines.NAME (or tali.value("NAME"), which
falls back to defines when no live input owns that name). In live preview the
Python cell executes after the page loads, so its values arrive a moment after
the {js} cells; a {js} cell reads them and re-runs automatically when they land
(and rebinds when a Python input changes), so the cell recomputes in place without
a reload. Guard against the value not having arrived yet with an early return.
# sample a noisy sine wave
=
const signal = tali..;
return;
return Plot.;