Tutuca Tutorial

This tutorial teaches tutuca step by step. It starts with the simplest component and ends with macros and async requests. Each section contains live code that you can edit. Change an example and press Ctrl+Enter (or Cmd+Enter on macOS) to see your changes. Later sections build on earlier sections. Read them in order.

Notation Reference

Tutuca templates use prefix characters to show different kinds of references. You will see them throughout this tutorial:

Basics

Minimum Viable Component

The simplest component has no fields, no view, and no logic: component({}). An empty object is the minimum for a tutuca component. This component does not render anything useful. But it shows that all parts are optional.

Static View Component

A view gives the component something to render. Here the view has no dynamic content. It is static HTML wrapped in the html tagged template. This works. But for static content with no state and no interactivity, use macros.

The html tag helps editors highlight and format the content as HTML. The css tag does the same for styles. Both tags are optional: you can use plain strings instead.

Text Rendering

Put @text on an existing element to add text before its other children, or use <x text=".field"></x> as a standalone text node with no extra DOM element. The value can be a field reference like .str. Tutuca then reads the str field. Or it can be a method call like $getStrUpper. Tutuca then calls the method and shows its return value. All value types are supported: strings, numbers, booleans, and null.

Mental Model

Three rules explain everything that follows. The application state is a single immutable root value. Every component instance is a node in that tree. The state is deeply frozen between transactions. The view is a pure function of the value: the same value always gives the same DOM. Every event handler receives a mutable draft as its first argument. The framework produces the new value and puts it into the tree. The renderer reuses cached subtrees by === identity. Every dispatched handler receives an Immer draft first, while this stays the immutable current instance.

In practice a click flows like this: click → handler changes its draft → Immer produces a new value → the framework puts the new root in place → the renderer compares with the previous frame and updates the DOM. You never subscribe or unsubscribe. There is no central store. Handlers do not change state in place. The rest of this tutorial teaches the syntax for these handlers and views. The model under them is what you just read.

State and Updates

Field Types

A component declares its state in fields, as a map of name to default value. Tutuca infers the field's type from the default: 0 is a number, "hi" a string, true a boolean, [] an Array, {} a plain object, new Map() a map, new Set() a set, and null the any type. Handlers update fields with normal JavaScript operations on their first draft argument.

Return nothing to commit the changed draft. Return another component instance to replace that component. If nothing changed, tutuca keeps the original identity. Generated field setters are not part of the API. Define only the named handlers that your view needs.

component({
  name: "Profile",
  fields: {
    name: "",        // string
    age: 0,          // number
    active: true,    // boolean
    tags: [],        // native Array
  },
  receive: {
    setName(draft, name) { draft.name = name; },
    toggleActive(draft) { draft.active = !draft.active; },
    addTag(draft, tag) { draft.tags.push(tag); },
  },
});

Methods vs Receive Handlers

Tutuca separates read-only computations from mutation handlers. methods are reads without arguments, used in value slots. receive holds the draft-first event and message handlers:

Every @on.* uses the bare form. It resolves only in receive; tutuca rejects $method there. When the linter finds the old dual-role pattern, it tells you to move the same-named method into receive.

Attribute Binding

Use the :attr syntax to bind component state to an HTML attribute. For example, :value=".str" binds the str field to the value attribute of the input. Template interpolation also works in bindings: :title="$'Content is {.str}'" builds a dynamic title string.

When the user types, @on.input="setStr e.value" calls the draft-first setStr handler with the current value of the input. For the number input, setRawNumber parses and checks the value before it assigns to the draft.

Event Handling

The + button uses @on.click="inc". This names the inc receive handler. The - button names dec in the same way. Both handlers change the first draft argument.

Computed methods never receive a draft. They cannot be event handlers. So calls on component instances are read-only by design.

Conditional Display

@show=".field" hides the host element when the field is falsy. @hide=".field" hides it when the field is truthy. The element still renders in the DOM; only its visibility changes. Use this for regions that toggle fast, when mount cost does not matter.

@show and @hide can also appear directly on the <x> render ops (render, render-it, render-each, text). They do not toggle a host element. They wrap the produced node in the same guard with no extra DOM. For example, <x render-it @show=".isOpen"></x> equals render-it wrapped in @show=".isOpen". When both appear on one element, the first attribute in source order becomes the outer wrapper.

The value can be a field, a built-in boolean predicate applied to one (empty?, truthy?, falsy?, null? — written predicate-first, e.g. @hide="empty? .items"), or a methods entry: @show="$canSubmit" calls methods.canSubmit() with no arguments and uses its return value. Use a method when a condition combines several fields; the predicates cover only single-field checks.

Event Modifiers

Modifiers filter events. A handler fires only under the specified conditions. @on.keydown+send="setLastSentSearch value" fires only when the user presses Enter. @on.keydown+cancel="resetQuery" fires only on Escape. Append modifiers with +; you can combine them (e.g. +ctrl+send).

Two modifiers act on the event itself. They do not filter: +prevent calls preventDefault() and +stop calls stopPropagation(). Both work on any event. Both run only after the filter modifiers on the same handler pass. So @on.keydown+send+prevent prevents the default on Enter and leaves every other key unchanged.

The example below also uses @show="truthy? .lastSentSearch" to show the search result conditionally — truthy? is one of the boolean predicates introduced in Conditional Display above.

Conditional Attributes

@if.class=".isActive" tests the boolean field. Then @then="'btn btn-success'" or @else="'btn btn-ghost'" sets the class. The same pattern works for any attribute: @if.title / @then.title / @else.title sets the title conditionally. Single quotes inside the value ('...') mark a string literal.

$toggleIsActive in the example is a small explicit handler that negates the draft field. The value for @if can also be a method — same rule as @show. This is useful when the predicate combines several fields.

With one @if directive, @then and @else do not need to specify the attribute name; they infer it from @if.<attr>. With several @if directives on one element, the additional @then and @else must name the attribute explicitly (e.g. @then.title, @else.title). HTML does not allow duplicate attribute names, so the parser would drop a second unsuffixed @then= before tutuca reads it.

Tabbed UI

A tabbed UI uses the two previous sections together. The active tab is one string field, tab. Everything else derives from it. There are no per-tab boolean flags and no separate component per tab.

The comparison uses equals?, a built-in boolean predicate. The unary predicates in Conditional Display (empty?, truthy?, falsy?, null?) take one value. equals? takes two values, written predicate-first: here a field and a string literal, as in equals? .tab 'overview'. Each tab panel uses it in @show, so only the matching panel is visible. Each tab button uses it in @if.class@then="'tab tab-active'" highlights the active button, @else="'tab'" styles the others.

A click calls $setTab 'overview'. This explicit handler assigns the tab draft field. Single quotes mark string literals both in the equals? comparison and in the handler argument.

No Dotted Paths in Values

A tutuca expression resolves a single name on this. Writing @text=".user.name", :value=".item.title", or @show=".item.isOpen" does not navigate. The parser never walks a dotted path where a value is read. Remember this rule. When the value is one level deeper, you have three options:

Exceptions: @each / render-each accept only .field or *dynamic (not a $method). <x render> expects a component instance — for a derived list, store it in a field or use @when with alter.

Quoting & String Literals

The tutuca expression parser depends on context. The rules for static strings differ from the rules for dynamic expressions. Learn these rules once; they prevent most syntax errors.

Plain HTML attributes are static strings as written (class="card"). Macro parameters passed without : are static strings too (label="Sale"). The quoting rules apply only when the prefix : turns the value into an expression, or inside @if / @then / @else / @text / event handler arguments.

Collections

List Iteration

@each=".items" iterates over the items field and repeats the element for each entry. Inside the loop, @key is the current index (for Lists) or key (for Maps), and @value is the current item. These are local bindings accessed with the @ prefix: @text="@key" and <x text="@value">.

List Filtering

Add @when="filterItem" next to @each. Tutuca then calls alter.filterItem(_key, item) for each entry. If it returns false, tutuca skips the item. Functions in the alter object have this bound to the component state, so filterItem here reads this.query and filters items by the current search string.

If the value used method syntax (@when="$filterItem"), tutuca would call a method instead. Like input, the alter section exists for organization only.

Iteration Enrichment

@enrich-with="enrichItem" calls alter.enrichItem(binds, _key, item) for each iteration step. By mutating the binds object (e.g. binds.count = item.length), you create new local bindings accessible in the template as @count. This lets you derive and display per-item values without adding them to the component's state.

Shared Iteration Data

@loop-with="getIterData" calls alter.getIterData(seq) once before the loop starts. Its return value (here { totalChars, queryLower }) goes as the third argument to both filterItem and enrichItem. This prevents duplicate work: queryLower is computed once, not per item, and totalChars comes from the full sequence before any filtering.

Filter and Paginate

A filter together with pagination is common, but order matters: @when filters within the slice @loop-with already cut. So a page can show fewer rows than its size, and the page count reflects the unfiltered total. To filter before paging, return keys from @loop-with: an ordered list of the matching rows' original keys for the current page.

The handler owns the whole pipeline: filter, sort, then slice. Because keys are the original indices, @key keeps the identity of each row: editing or deleting a row on page 2 of a filtered view hits the correct item. A keys return is authoritative, so the renderer does not apply @when again. Each row here is its own <x render-it> component, so its fields are self-contained two-way bindings.

The page controls sit outside the loop, so they cannot see its per-loop data. The pattern below splits the work in two: a scope @enrich-with on the <section> does one counting scan and publishes the clamped page and labels as @-bindings for the controls to read. The @loop-with handler then takes a context, (seq, { lookup, filter }). It reads that clamped page via lookup, reuses the declared @when via filter, and collects only the current page's keys — it scans only as far as needed.

Scope Enrichment

Use @enrich-with="enrichScope" on an element without @each. The alter function then returns an object (here { len, upper }) whose keys become @-prefixed bindings for all child elements in this view. (They reach plain DOM descendants, not a rendered child component; a <x render> boundary starts a clean namespace.) Inside the enriched <div>, @len and @upper are available next to the regular field bindings. This helps you put derived values into a section of the template without storing them in component state.

Rendering Components

Rendering a Child Component

Until here, components were self-contained: their views render only their own fields. <x render=".field"> renders a child component held in a field of the current component. The child draws its own view from its own state, scoped to its field. <x render-it> works the same way, but only inside an iteration (@each or render-each). It renders the component instance of the current iteration.

A rendered child gets a clean namespace: the parent's @ bindings (from @each iteration or @enrich-with) and scope enrichments do not cross into it. The child sees only its own fields. To pass a value across that boundary, use a dynamic binding (*name).

With children, the value tree becomes a real tree: each <x render> is a parent → child edge. A click inside the grandchild produces a new grandchild. That produces a new child. That produces a new root (see Mental Model).

Multiple Views

A component defines its default template in view and alternate templates in views: { name: html`...` }. <x render=".item"> renders the default ("main") view. Adding as="edit" selects the named "edit" view instead. Both render the same Entry instance — the main view shows read-only text, while the edit view shows input fields bound to the same fields.

Collection Item Access

<x render=".byIndex[.currentIndex]"> renders the component at position .currentIndex in the byIndex list. The bracket syntax resolves the inner expression as a key into the outer collection. .byKey[.currentKey] does the same for a native Map: it looks up the entry by string key.

The range slider updates currentIndex, and the select dropdown updates currentKey. The rendered component then updates.

Dynamic View Switching

@push-view=".view" puts a view name onto the rendering stack. When tutuca renders a component, it looks for the view name from the top of the stack down. It uses the first defined match; if none matches, it renders the default "main" view. The view stack applies to any component rendered recursively under the @push-view directive, not only direct children.

Change the view field between "main" and "edit". Every Entry item then switches between read-only and editable mode at once. The @when="filterItem" attribute on <x render-each> filters items in the same way @when does on @each.

Component Styles

style: css`...` applies to one component and view combination: the same class name .mine can have different styles in different views. commonStyle is shared by all views of the same component. globalStyle is added globally with no scoping.

Both style and commonStyle are wrapped in a component-scoped selector ([data-cid="N"]{ … }). Bare declarations with no selector (e.g. color: red;) go directly inside that wrapper, so they style the root element of the component; you need no extra wrapper selector. Rules written with a selector (.mine { … }) target descendants instead.

View "two" defines its own style that overrides the default. Notice .mine is red in the main view but orange and underlined in view two. The root component renders all three views side by side using <x render=".value" as="viewName">.

Statics

statics: { ... } puts methods on the component class, not on instances. Call them as Comp.fromData(...)component({...}) returns the class itself, with the component's metadata attached behind a well-known symbol. Inside a static, this is the class itself, so this.make({...}) calls the generated constructor that component({...}) makes for every component.

The most common use is a fromData factory that builds an instance from plain JS data (e.g. JSON loaded from disk) and constructs children recursively. This is what the recursive-tree example in the next section uses: it turns a nested object literal into a fully constructed TreeItem with arrays of TreeItem children. Statics are not part of any lifecycle. They are plain class methods, called by the host application or by another static.

A component binds to a scope at registerComponents time, and that scope owns it (and therefore its scope-bound make / statics). So a given component object is live in one scope at a time. Note this case: a static that builds a different child type names the imported const directly (e.g. TreeItem.fromData(v)) and hardcodes that child's original scope. That works in a single-scope app; if you run the same definition in separate registries, resolve the child through the caller's scope instead — this.scope.lookupComponent("TreeItem").fromData(v). Recursion into the same type needs no lookup: just call this.fromData(v) / this.make(...).

Recursive Components

TreeItem renders its children with <x render-each=".items">, where .items is a list of more TreeItem instances: the component renders itself, recursively. Each render creates a new VDOM subtree for that branch, and tutuca stops descending when an item has an empty items list. TreeItem.fromData (a static, see the previous section) builds the nested structure from plain data. The component style uses the CSS pseudo-element :before to show folder / file icons based on class names set with @if.class.

Component Communication

Components do not need to talk to each other; many apps let parents read the state of children directly. But sometimes one component must tell another to do something. Tutuca offers two channels, and one question separates them: does the sender know who handles this? If yes, it sends a message. A message reaches one component and stops. If no, it raises an intent. An intent walks a route until something answers. Dynamic bindings cover the third case: a descendant needs read-only access to an ancestor's value without passing it through every component in between.

Send / Receive

ctx.send(name, args) calls a named handler in receive: { ... }. Called bare, it targets the current component. Prefixed with ctx.at, it targets any component reachable by path. ctx.at returns a path builder with .field(name), .index(name, i), and .key(name, k). Chain calls to descend further, then end with .send(name, args).

When to send (vs raise an intent). Send delivers a message to a specific target by path. Use it when one component needs to address another by name (a form tells its email field to focus, a list tells item 3 to enter edit mode), or to call a receive handler on self from several call sites without duplicating its body. When you do not know who should answer, raise an intent instead and let a route find someone (next section).

In the example below, the form sends ctx.at.field("status").send("flash", [text]) to its sibling Status child on submit. It sends ctx.send("clearDraft") to itself to reuse the same reset handler from a second call site. The Status component owns its own state and knows nothing about who sent the message: the path is the only coupling.

Intents and Routes

When to raise an intent: handle the action locally if the current component owns the state needed to respond. Raise an intent when you do not know who should answer: a list item's remove that must reach the list owning the items, or something an ancestor may want to record (selection, logging, analytics).

An intent carries a route: a list of legs saying where to look. "dyn" walks the dispatch path from the sender's parent up to the root; "lex" walks the handlers registered on the scope (async work lives there, in the next section). With no route it takes both, in that order. The verb does not decide which scope answers; the route does, and it is written at the call site where the decision is.

The recursive tree.js example from Recursive Components shows this. When a node is clicked, onItemClick calls ctx.intent("treeItemSelected", [this], { route: ["dyn"] }). Tutuca walks the component path from the sender's parent toward the root and, at each ancestor, looks for a matching intent.treeItemSelected handler; in the tree example the TreeRoot at the top of the chain catches it and adds a log entry at the front.

Intent handlers return a (possibly updated) instance of their own component, just like methods and receive handlers. One sentence governs the walk: a reply ends the walk; running does not. A handler that changes state and returns is an observer, and the intent keeps going. One that calls ctx.reply(value) or ctx.fail(error) answers and stops the walk. ctx.stop() ends the walk with no answer. That single rule removes the need for a separate "listener" bucket: an observer and an answerer are the same construct with and without a reply.

The answer comes back to the sender as an ordinary message, in the receive bucket, under one of three names: <name>Ok with the result, <name>Error with the error, or <name>Unhandled (carrying the intent's own arguments) when the route ran out with nobody claiming it. Each arm takes a single payload, so no handler can be handed both a result and an error and read the wrong one. Declaring these arms makes an intent a request rather than a notification. Declare none and tutuca simply drops the outcome.

Async Requests

Lifecycle note: tutuca has no built-in lifecycle. The receive section is only a place to register named handlers; nothing in the framework calls receive.init automatically. The host application must dispatch it (the tutorial harness calls app.sendAtRoot("init") after app.start() — that is what makes init run in these playgrounds).

The init(ctx) handler in the receive section runs when the application starts. It calls ctx.intent("loadData", [], { route: ["lex"] }) to trigger the async function registered in getIntentHandlers(). When the fetch completes, tutuca delivers the result to receive.loadDataOk(res). If it threw, the result goes to receive.loadDataError(err). If nothing was registered to answer it, it goes to receive.loadDataUnhandled().

The component manages a loading state with @show=".isLoading" and @hide=".isLoading". The "Load Another Way" button raises the same intent on the default route (["dyn", "lex"]): the ancestors first, then the registered handlers. The answer arms do not change with the route: the route says who answers, the arms say what to do about it.

The async implementation sits outside the component in getIntentHandlers(). This separation lets the same component behave differently in production, in different test cases, or in different apps: change only the registered handler. A handler that has nothing to contribute returns the exported PASS sentinel to decline, and the walk moves on to the next one.

Dynamic Bindings

Dynamic bindings reach across the tree: a producer component publishes a value, and any descendant can read it as *name without the value being passed through every component in between.

Best practice: keep state local to the component. Use provide / lookup only when it is genuinely the only solution: a value owned far away that a deep descendant needs, and nothing in between should know about. Dynamic bindings couple a consumer to a producer that may not be in scope. So keep components as self-contained as possible: let a child render the field it needs from its owner, and lift state only as far up the tree as it needs to live.

A producer declares provide: { entries: ".items" }: the field (or seq-access) it wants to expose. Tutuca evaluates every provide and puts it onto the stack automatically when render enters the producer; there is no hook to opt in. A consumer declares lookup: [{ name: "entries", default: ".items" }] — a list of the names it wants, not of who provides them — then reads one as *entries in the template (e.g. @each="*entries"). When no producer is in the render stack, tutuca uses the default expression (omit it to get null).

Dynamic Render Targets

A *name dynamic var resolves to a value, so it works anywhere a value is read, not just inside :style / :class. In particular it can be a component-render target: <x render="*name"> renders the component the dynamic points at, as="edit" selects one of its views, and @each="*name" iterates it when the dynamic resolves to a sequence.

A provide value must be addressable: a field or a .seq[.key] seq-access. It doubles as the render-target and mutation path, so a method or constant gives a lint error. A producer can therefore expose a single selected entry with provide: { selected: ".items[.selectedKey]" }. There is no *name[.key] form: the consumer never indexes a dynamic var, it just reads the resolved value as *name. (A lookup default, by contrast, is only a value fallback; it may be any expression, including a constant.)

In the example below, Workspace exposes its .sheet field as the dynamic active and renders a deep tree (Workspace → Panel → Toolbar). Toolbar, far from the producer, consumes active and renders <x render="*active">. The rendered Sheet lives at Workspace.sheet, so when you edit its title the mutation goes directly there. Every state update in tutuca runs as a transaction: a dispatched handler call that swaps the new state tree in for the old one. Here the render path is expanded to reconstruct the handler, but the transaction skips the intermediate components and lands on Workspace.sheet. The title echoed at the top proves it: it updates with every change.

Macros

Macros: Reusable Templates

Macros define reusable HTML fragments that expand in place. macro({}, html`...`) takes a defaults object (empty here) and a template. Export macros via getMacros() and reference them in templates with the <x:name> syntax. Unlike components, macros have no state or lifecycle: they are pure template expansion. Use them for repeated markup patterns.

The HTML parser lowercases custom tag names, so <x:Card> is read as <x:card>. Registry keys become lowercase on registerMacros, so a capitalized const like { Card } registers under card. Two different macros under the same lowercased name cause a warning via console.assert.

Macros: Parameters

Macros take parameters with default values. The first argument to macro() defines the defaults: { label: "'New'", kind: "'info'" }. Inside the macro template, ^param references a parameter. For example, @text="^label" displays the label value.

In the macro tag, a plain attribute like label="Sale" passes a static string. No quotes are needed; this is the same as regular HTML attributes. A dynamic attribute (prefixed with :) takes an expression instead, following the rules from Quoting & String Literals: :label="'Sale'" is a string literal, :label=".status" a field reference.

Macros: Slots

<x:slot></x:slot> inside a macro template acts as a placeholder for child content. Children placed inside the macro tag replace the slot when the macro expands. Layout macros use this: cards, panels, and containers wrap arbitrary content while providing consistent structure and styling.

Macros expand inline into the calling component's template. So @on.click="inc" inside a macro calls the host component's receive.inc handler, never anything on the macro itself (macros have no state or methods). This differs from components: a component encapsulates its own state and handlers, while a macro is only template expansion in the context of its host component.

Macros: Named Slots

A macro can define several insertion points with named slots. Inside the macro template, <x:slot name="actions"> and <x:slot name="footer"> mark named slots, while <x:slot> (the same as <x:slot name="_">) is the default slot.

Wrap content in <x slot="name"> to target a specific named slot. Children not wrapped in a named <x slot> go to the default slot. This way macros define complex layouts with several customizable regions.

Special Cases

Drag and Drop

Set draggable="true" to enable drag on each item. data-dragtype declares the drag type of the element (e.g. "my-list-item"), and data-droptarget marks it as a valid drop zone.

During a drag, tutuca manages two runtime attributes: data-dragging="1" is set on the source element while it is dragged, and data-draggingover is set on the current drop target with the value of the source's data-dragtype. Use these as CSS attribute selectors to style drag states. For example, [data-dragging="1"] fades the source and [data-draggingover="my-list-item"] highlights the target. Tutuca removes both runtime attributes automatically when the drag ends.

The drop handler receives @key (the target index), dragInfo, and event (the raw DOM event). dragInfo captures the rendering stack from when the dragged element was rendered, so dragInfo.lookupBind("key") returns the source item's iteration index, or any other binding that was available at that point (see Mental Model — this is the same stack that resolves @key / @value during render). The component style (using the css tagged template) adds visual feedback for dragging states, scoped to this component.

Web Components

Custom elements work as tags inside a component view, and any CustomEvent they fire is reachable via @on.<event-name>. The event's detail is what the built-in value handler arg resolves to, so an input handler signature like onEmojiClick(detail) receives the picker's detail object directly.

The example below imports emoji-picker-element from a CDN and listens for its emoji-click custom event. Put hyphenated event names straight into the @on. attribute; no special quoting is needed.

Pseudo-x (@x)

Tutuca's special operations (render, render-each, render-it, text, show, hide, slot) live on the <x> tag. That works almost everywhere, but the browser's HTML parser refuses to keep <x> (or any unknown tag) as a child of certain elements. <select> accepts only <option>, <table> accepts only <tr> / <tbody> / etc., and <tr> accepts only <th> / <td>. Put a <x render-each> inside any of those and the parser silently removes it.

The solution: prefix the first attribute on a legal child tag with @x. Tutuca treats that tag as if it were <x> and reads the next attribute as the special op. The host element itself is ignored; only the special op runs.

The example below shows both common cases. The first parent renders a <table> whose rows are themselves components: inside <tbody>, <tr @x render-each=".rows"> tells tutuca to render one TableRow component per item. The second parent renders a <select> whose options are components: <option @x render-each=".options"> renders one SelectOption per item. The same pattern works inside <tr>, <colgroup>, <dl>, <details>, or anywhere else the parser would otherwise discard a <x> tag.

Raw HTML

@dangerouslysetinnerhtml=".content" sets the element's innerHTML from the field value. The name is intentionally strong (borrowed from React). It warns you: this directive bypasses all text escaping. If the content comes from untrusted sources, it can cause XSS attacks. Use it only when you control the HTML content or have sanitized it. While this directive is active, the element's children in the template are ignored.

Testing

Components have three testable layers: methods (called directly from JS), input handlers (template-attached event handlers), and iteration handlers in the alter block (used by @when, @loop-with, and @enrich-with). The testing tab in this playground runs the tests written in the same module as the component; the CLI command tutuca test <module> picks up the same tests.

Test Setup

A module joins testing by exporting getTests({ describe, test, expect }). expect is chai; describe and test are tutuca's own subset of the common Mocha/Jest-style API (no before / after / beforeEach). Group tests by component with describe(MyComp, () => { ... }). This tags the suite, so tutuca test <module> MyComp picks it up.

Calling Methods and Input Handlers

Methods bind to the instance; call them directly: MyComp.make().inc() returns the next instance. Input handlers are plain functions stored on the component descriptor with no this bound, so use .call to bind the instance explicitly: MyComp.input.dec.call(MyComp.make()). The arguments after the instance are exactly what the template would have passed (e.g. the resolved value / valueAsInt handler args).

Testing Iteration Handlers

The three iteration handlers in alter have distinct signatures: when(key, value, iterData) filters, loopWith(seq) runs once and produces shared iterData, and enrichWith(binds, key, value, iterData) changes each kept item's bindings. this is the parent component instance in all three.

Use collectIterBindings to test the whole pipeline (filter + loop-data + enrichment). A working implementation ships only in the dev build; the core tutuca build (the one the README's CDN quick start imports) exports a no-op stub. In your own project import it from "tutuca/dev", or run tests through tutuca test, which redirects the bare "tutuca" import to the dev build automatically. The playgrounds on this page import it from "tutuca" because their import map points that specifier at the dev build:

import { collectIterBindings } from "tutuca";

const c = MyComp.make({ items: [...] });
const r = collectIterBindings(MyComp, c, c.items, {
  loopWith: "loopHandlerName",     // optional
  when: "whenHandlerName",         // optional
  enrichWith: "enrichHandlerName", // optional
});
// r is Array<{ key, value, ...enrichments }> — one entry per kept
// item, in iteration order.

Handler names refer to entries in MyComp.alter; unknown names throw. The example below has computed methods, receive handlers, and an iteration pipeline — all three are exercised by getTests. The Test tab is selected automatically (auto-run-tests) so you see the result on load and after every Ctrl/Cmd+Enter.

Linter Reference

The tutuca CLI's lint command emits codes for the categories below. Run it after each edit (the post-edit recipe in the skill's core.mdVerifying changes). It catches typos and broken references before they reach a render. The example below triggers every category on purpose; open the Lint tab on the right to see them all detected.

The categories the linter reports, grouped as in the CLI reference:

What's Next

You now know all core features of tutuca. To see them working together in realistic scenarios, see the example apps on the home page: a to-do list, a JSON editor, a recursive tree, and more. The next step beyond this page is the storybook. Put your components and examples in a co-located *.dev.js module (a dev-only file exporting getComponents() and getExamples()) and tutuca storybook serves a live catalog of them with no setup. The Storybook here is a live reference for every feature of that format: sections, named views, macros, per-example request mocks, lifecycle on hooks, and drive tests. For the full API and source code, visit the GitHub repository.