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:
-
.name— component field or method (e.g..count,.inc) -
@name— local binding from iteration or scope enrichment (e.g.@key,@value) -
^name— macro parameter (e.g.^label) -
$name— computed property (e.g.$totalItemsChars)
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.
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=".isLastSentSearchSet"
conditionally displays the search result.
isLastSentSearchSet is auto-generated for nullable fields
(those initialized with null), returning
true when the field has a non-null value.
@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.
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.
Computed Properties
computed.totalItemsChars() sums character counts across
all items. It is referenced in the template as
$totalItemsChars (note the $ prefix). Unlike
@loop-with which runs on every render, computed values
are cached per component instance — the function only re-runs
when the instance itself changes (check the
console.log to verify). This makes computed properties
ideal for computation intensive values that should be calculated once
and lazily, on first view usage.
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">, creating a recursive
tree. statics.fromData is a factory method on the
component class that recursively builds the tree from plain objects.
When a node is clicked,
ctx.bubble("treeItemSelected", [this]) sends an event up
the component tree — each ancestor's
bubble.treeItemSelected handler can react to it (or not).
The root TreeRoot catches the event and logs the
selection. Component style uses CSS pseudo-elements
(:before) to display folder/file icons based on class
names set with @if.class.
Advanced Features
Async Requests
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.
Note: logic.init is called by the tutorial setup code
when the app starts. This is not a built-in behavior of the library
— you can do something else or nothing on init.
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.
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.
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.
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.