Cells ·
Dataflow ·
What the compiler sees ·
viewof ·
mutable ·
Promises and generators ·
Standard library ·
import ·
Errors ·
Printing a cell's source
For the widget library see @tomlarkworthy/inputs-reference.
A cell is an optionally named program whose value or value stream can be referenced by other cells.
| source | defines |
|---|---|
42 |
an anonymous cell |
answer = 42 |
answer |
total = { … return v; } |
total, from a block body |
settings = ({a: 1}) |
settings. Without the parens { opens a block and the value is undefined |
viewof x = element |
viewof x and x |
mutable y = 0 |
mutable y and y |
import {a} from "@user/nb" |
a |
answer = 6 * 7
sum_to_ten = {
let total = 0;
for (let i = 1; i <= 10; i++) total += i;
return total;
}
settings = ({ retries: 3, timeout: 1000 })
A cell's inputs are the identifiers in its body. When the value of an input changes, the cell is re-run by the Observable Runtime.
viewof celsius = Inputs.range([-40, 100], {value: 20, step: 1, label: "°C"})
fahrenheit = celsius * 9 / 5 + 32
this is the cell's previous value, so a cell can fold over its own executions. reruns has one input, so it counts how often the slider has caused recomputation.
reruns = {
celsius;
return (this ?? 0) + 1;
}
Cells are mapped to one or more reactive variables. The @tomlarkworthy/observablejs-toolchain is able to map to and from the lower-level reactive variable representation. Reactive variables have an inputs array, a definition and an optional name.
A cell that fails to parse compiles to a SyntaxError holding its _sourceExpression, so even broken cells can be decompiled back to their original.
viewof name = element is a special construction designed for UI widgets. A viewof defines two cells: viewof name is the UI element, and name is element.value, which emits a new value every time the UI changes (specifically triggered by an Event('input') from the UI element).
Inputs is a library of these, but there is nothing special about them. viewof point below is hand-written.
viewof fruit = Inputs.select(["🍎", "🍌", "🍇"], {label: "fruit"})
fruit is 🍎
viewof point = {
const el = htl.html`<svg width="240" height="120" style="border:1px solid var(--theme-foreground-faint); touch-action:none">
<circle r="6" fill="currentColor"></circle>
</svg>`;
const dot = el.querySelector("circle");
const set = (x, y) => {
el.value = {x: Math.round(x), y: Math.round(y)};
dot.setAttribute("cx", el.value.x);
dot.setAttribute("cy", el.value.y);
el.dispatchEvent(new Event("input", {bubbles: true}));
};
el.onpointermove = (event) => {
if (event.buttons === 0) return;
const box = el.getBoundingClientRect();
set(event.clientX - box.left, event.clientY - box.top);
};
set(120, 60);
return el;
}
drag in the box — point is 120, 60
mutable name = value defines a cell that other cells can write into.
mutable clicks = 0
clicks_button = {
const button = htl.html`<button type="button">+1</button>`;
button.onclick = () => ++mutable clicks;
return button;
}
clicks is 0
Cells can return a promise and the Runtime will wait for it before triggering downstream cells.
Cells can also emit streams of values by yielding. The internal representation is then a generator and the runtime pulls one value per animation frame, re-running downstream cells each time.
Descendants of a fast generator miss values: the graph converges on the latest state rather than processing a stream (responsiveness).
later = Promises.delay(1000, "…arrived a second late")
viewof clock_running = Inputs.toggle({label: "run the clock", value: true})
clock = {
if (!clock_running) {
yield "paused";
return;
}
while (true) {
yield new Date().toLocaleTimeString();
await Promises.delay(1000);
}
}
invalidation is a promise that resolves when the cell is recomputed, useful for undoing side effects or releasing resources. Timers, sockets and listeners have to be released there or they outlive the cell. Delete the .then below and the setInterval keeps counting until the page closes.
invalidation_example = {
const el = htl.html`<span>0s</span>`;
let seconds = 0;
const timer = setInterval(() => (el.textContent = `${++seconds}s`), 1000);
invalidation.then(() => clearInterval(timer));
return el;
}
Certain functions are available to all modules without importing them. The bootloader sets them up before other modules load.
import {a, b} from "@user/notebook" // by name
import {a as alias} from "@user/notebook" // renamed
import {viewof v, mutable m} from "@user/notebook" // views and mutables
import {chart} with {sales as data} from "@user/nb" // injection
Import cells bring references from other modules into the module's scope. These are live reactive references.
linkTo, imported from @tomlarkworthy/lopepage-urls, turns a module and cell into a lopepage link: Inputs.range.
An error stays in the cell that raised it. Cells downstream report the same error, and do not recompute. Errors can be used to deliberately stop reactive propagation.
| message | cause |
|---|---|
x is not defined |
nothing defines x: no cell, no import, no builtin |
x is defined more than once |
two cells claim the name |
circular definition |
a cell's inputs lead back to itself |
a SyntaxError when the cell runs |
the body did not parse |
| a cell pending forever | an await or a generator that never resolves |
lookupVariable finds a cell's runtime variables by name and decompile turns them back into source. This is just used here to help drive the examples on this page.
show = {
liveCellMap; // an input only so that editing any cell re-prints every source below
const group = (name) =>
name.startsWith("viewof ")
? [name, name.slice(7)]
: name.startsWith("mutable ")
? ["initial " + name.slice(8), name, name.slice(8)]
: [name];
// observablehq.com's own compiler names these viewof x / mutable x; print the source spelling
const unmangle = (src) =>
src
.replace(/\bmutable\$([A-Za-z_$][\w$]*)\.value\b/g, "mutable $1")
.replace(/\b(viewof|mutable)\$([A-Za-z_$][\w$]*)/g, "$1 $2");
return async function show(name) {
if (!refModule) return md`\`${name}\` — module not resolved yet`;
const variables = await lookupVariable(group(name), refModule);
if (variables.some((v) => !v)) return md`\`${name}\` — no such cell`;
return md`\`\`\`js
${unmangle(await decompile(variables))}
\`\`\``;
};
}