Tutuca Tutorial

This tutorial walks you through tutuca's features step by step, from the simplest possible component to macros and async requests. Each section includes editable live code — modify the examples and press Ctrl+Enter (or Cmd+Enter on macOS) to see your changes instantly. Concepts build on each other, so working through them in order is recommended.

Notation Reference

Tutuca templates use prefix characters to distinguish different kinds of references. You will encounter these throughout the tutorial:

Basics

Minimum Viable Component

The simplest possible component is one with no fields, no view, and no logic: component({}). This is the absolute minimum required to create a tutuca component — an empty object is all you need. It won't render anything useful, but it demonstrates that everything is optional.

Static View Component

Adding a view gives the component something to render. Here the view contains no dynamic content — just static HTML wrapped in the html tagged template. This works, but for purely static content with no state or interactivity, macros are a better fit.

The html tag is just a hint for editors to syntax highlight and format the content as HTML — the same applies to the css tag for styles. Both are optional: you can use plain strings instead.

Text Rendering

Use @text as an attribute on an existing element to prepend text into it (preserving its other children), or use <x text=".field"></x> as a standalone text node that adds no extra DOM element. The value can be a field reference like .str, which resolves the str field, or a method call like .getStrUpper, which invokes the method and displays its return value. All value types are supported: strings, numbers, booleans, and null.

Data Binding

Attribute Binding

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

When the user types, @on.input=".setStr value" calls the auto-generated setStr setter with the input's current value. For the number input, a custom method setRawNumber parses and validates before calling the auto-generated setNum.

Quoting & String Literals

Tutuca's expression parser is context-sensitive, and the rules for static strings differ from the ones for dynamic expressions. This is the most-tripped-over piece of the syntax, so it is worth understanding once.

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

Event Handling

The + button uses @on.click=".inc" — the dot prefix calls a methods function. The - button uses @on.click="dec" — no dot means it calls an input handler. Both return new state via the auto-generated this.setCount().

Methods and input handlers both must return a new component instance. The input section is just for organization purposes, you can use methods if you want.

Event Modifiers

Modifiers filter events so handlers only fire under specific conditions. @on.keydown+send=".setLastSentSearch value" only fires when the user presses Enter. @on.keydown+cancel=".resetQuery" only fires on Escape. Modifiers are appended with + and can be combined (e.g. +ctrl+send).

Also shown here: @show=".isLastSentSearchTruthy" conditionally displays the search result. isLastSentSearchTruthy is auto-generated for primitive and nullable fields, returning true when the field's value is JS-truthy. Nullable fields (those initialized with null) also get isLastSentSearchNull for when you specifically need a null check. @show and @hide are covered in more detail in Conditional Attributes.

Conditional Attributes

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

@show and @hide toggle element visibility based on a field value. .toggleIsActive is auto-generated for boolean fields.

The value passed to @show, @hide, and @if can also be a method call — @show=".canSubmit" invokes methods.canSubmit with no arguments and uses its return value. Reach for this when a predicate combines multiple fields (the auto-generated isXTruthy / isXEmpty / isXNull only cover single-field checks). The same pattern works wherever a value is read — @text=".fullName", :title=".tooltip", :class="badge {.kindClass}".

Tutuca expressions resolve a single name on this — there is no path syntax. Writing @text=".user.name" or :value=".item.title" does not navigate into the field; the parser will not walk a dotted path anywhere a value is read. When the value you want lives one level deeper, you have three options:

show and hide can also appear as extra attributes on the <x> render ops (text, render, render-it, render-each) — instead of toggling a host element, they wrap the produced node in the equivalent conditional, so you get a guard with no extra DOM. For example, <x render-it show=".isOpen"></x> is the equivalent of render-it wrapped in @show=".isOpen". When both appear on the same element, the first attribute in source order becomes the outermost wrapper.

When there is a single @if directive, @then and @else don't need to specify the attribute name — they infer it from context. If there are multiple @if directives on the same element, the additional @then and @else must specify the attribute name explicitly (e.g. @then.title, @else.title).

Component Styles

style: css`...` is scoped to a specific component+view combination — the same class name .mine can have different styles in different views. commonStyle is shared across all views of the same component. globalStyle is injected globally without scoping.

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">.

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

Adding @when="filterItem" alongside @each calls alter.filterItem(_key, item) for each entry. If it returns false, the item is skipped. Functions in the alter object have this bound to the component state, so here it reads this.query to filter items by the current search string.

If the value used the dot syntax (@when=".filterItem"), it would call a method instead. Like input, the alter section is for organizational purposes 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 }) is passed as the third argument to both filterItem and enrichItem. This avoids redundant computation — queryLower is computed once instead of per-item, and totalChars is calculated from the full sequence before any filtering.

Scope Enrichment

When @enrich-with="enrichScope" is used on an element without @each, the alter function returns an object (here { len, upper }) whose keys become @-prefixed bindings for all children. Inside the enriched <div>, @len and @upper are available alongside the regular field bindings. This is useful for injecting derived values into a section of the template without storing them in component state.

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 an IMap (immutable Map), looking up the entry by string key.

The range slider and select dropdown update currentIndex and currentKey respectively, and the rendered component updates in response.

Views & Rendering

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.

Dynamic View Switching

@push-view=".view" pushes a view name onto the rendering stack. When rendering a component, tutuca looks for the view name starting from the top of the stack and keeps trying until it finds the first one that is defined; if none is found it renders the default "main" view. The view stack applies to any component rendered recursively under the @push-view directive, not only direct children.

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

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. The component style uses CSS pseudo-elements (:before) to show folder / file icons based on class names set with @if.class.

Bubble Events

The same tree.js example also demonstrates ctx.bubble. When a node is clicked, onItemClick calls ctx.bubble("treeItemSelected", [this]). Tutuca walks the component path from the source up toward the root and, at each ancestor, looks for a matching bubble.treeItemSelected handler. Ancestors without a handler are skipped silently; in the tree example the TreeRoot at the top of the chain catches the event and prepends a log entry.

Bubble handlers return a (possibly updated) instance of their own component, just like methods / input / logic handlers. Walking stops when the root state value is reached or when a handler calls ctx.stopPropagation(). This is how aggregate state (logs, selections, totals) is maintained without prop-drilling callbacks down the tree.

Statics

statics: { ... } attaches methods to the component class rather than to instances. They are called as Comp.Class.fromData(...). Inside a static, this is the class itself, so this.make({...}) calls the auto-generated constructor that component({...}) generates 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), recursively constructing children. In tree.js, TreeItem.Class.fromData maps each child object back through itself, so an entire nested object literal becomes a fully constructed TreeItem instance with List<TreeItem> children. Statics are not part of any lifecycle — they are plain class methods, called by the host application or by another static.

Advanced Features

Async Requests

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

The init(ctx) handler in the logic section is called when the application starts. It calls ctx.request("loadData", []) to trigger the async function registered in getRequestHandlers(). When the fetch completes, tutuca calls response.loadData(res, err) with the result.

The component manages a loading state with @show=".isLoading" and @hide=".isLoading". The "Load Another Way" button demonstrates ctx.request() with custom callback names via onOkName and onErrorName options, routing the response to different handlers.

Notice how the request implementation is defined outside the component in getRequestHandlers(). This separation means the same component can behave differently in production, in different test cases, or even in different apps — just by changing the request handler.

Drag and Drop

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

During a drag, tutuca automatically manages two runtime attributes: data-dragging="1" is set on the source element while it's being dragged, and data-draggingover is set on the current drop target with the value of the source's data-dragtype. You can use these as CSS attribute selectors to style drag states, for example, [data-dragging="1"] to fade the source and [data-draggingover="my-list-item"] to highlight the target. Both runtime attributes are cleaned up 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") can retrieve the source item's iteration index — or any other binding that was available at that point. The component style (using the css tagged template) adds visual feedback for dragging states, scoped to this component.

Web Components

Custom elements work as drop-in 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. Hyphenated event names go straight into the @on. attribute, no special quoting needed.

Dynamic Bindings

Dynamic bindings are tutuca's prop-drilling escape hatch: a producer component publishes a value, and any descendant can read it as *name without the value being passed through every component in between.

A producer declares dynamic: { entries: ".items" } — the field (or expression) it wants to expose — and lists the names it publishes via on: { stackEnter() { return ["entries"]; } }. A consumer declares dynamic: { entries: { for: "EntryEditorAndSelector.entries", default: ".items" } } to alias the producer's name into its own scope, then reads it as *entries in the template (e.g. @each="*entries"). When no producer is in the render stack, the default expression is used.

Macros

Macros: Reusable Templates

Macros let you 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, making them ideal for repeated markup patterns.

The HTML parser lowercases custom tag names, so <x:Card> is read as <x:card>. Registry keys are normalized to lowercase on registerMacros, so a capitalized const like { Card } works fine — it registers under card. Registering two different macros under the same lowercased name warns via console.assert.

Macros: Parameters

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

When using the macro, a plain attribute like label="Sale" passes a static string — no quotes needed, just like regular HTML attributes. If the attribute is dynamic (prefixed with :), the value is an expression, so string literals must be single-quoted to distinguish them from field references: :label="'Sale'" is the dynamic equivalent. Without quotes, :label=".status" resolves the component's status field instead.

Macros: Slots

<x:slot></x:slot> inside a macro template acts as a placeholder for child content. Any children placed inside the macro tag replace the slot when the macro expands. This enables layout macros like cards, panels, and containers that wrap arbitrary content while providing consistent structure and styling.

Because macros expand inline into the calling component's template, @on.click=".inc" inside a macro calls inc on the component where the macro is used — not on the macro itself (macros have no state or methods). This is a key difference from components: a component encapsulates its own state and handlers, while a macro is just template expansion that operates in the context of its host component.

Macros: Named Slots

A macro can define multiple insertion points using named slots. Inside the macro template, <x:slot name="actions"> and <x:slot name="footer"> mark named slots, while <x:slot> (or equivalently <x:slot name="_">) is the default.

When using the macro, wrap content in <x slot="name"> to target a specific named slot. Any children not wrapped in a named <x slot> go to the default slot. This allows macros to define complex layouts with multiple customizable regions.

Escape Hatches

Raw HTML

@dangerouslysetinnerhtml=".content" sets the element's innerHTML from the field value. The intentionally scary name (borrowed from React) warns that this bypasses all text escaping — if the content comes from untrusted sources, it opens the door to XSS attacks. Use it only when you control the HTML content or have sanitized it. When this directive is active, the element's children in the template are ignored.

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> only accepts <option>, <table> only accepts <tr> / <tbody> / etc., <tr> only accepts <th> / <td>. Drop a <x render-each> in any of those and the parser silently strips it.

The escape hatch: 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 demonstrates 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 trick works inside <tr>, <colgroup>, <dl>, <details>, or anywhere else the parser would otherwise discard a <x> tag.

Common Mistakes

The example below intentionally triggers every category the linter can catch. Open the Lint tab on the right to see all of the issues detected. The tutuca CLI's lint command runs the same checks from the command line.

The categories the linter reports:

What's Next

You've covered all of tutuca's core features. To see them working together in more realistic scenarios, check out the example apps on the home page — including a to-do list, a JSON editor, a recursive tree, and more. For the full API and source code, visit the GitHub repository.