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). Single-level only — paths like.user.nameare not supported anywhere. To reach a nested value, render the child as a component (<x render=".user">then@text=".name"inside), add a method that returns it, or expose it via@enrich-with. -
@name— local binding from iteration or scope enrichment (e.g.@key,@value) -
^name— macro parameter (e.g.^label) -
!name— request handler (e.g.!loadData) -
*name— dynamic binding (e.g.*theme,*entries)
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.
Mental Model
Before diving into more syntax, three rules anchor everything that
follows. The application state is a
single immutable root value — every component
instance is a node in that tree. The view is a
pure function of the value: render the same value,
get the same DOM. Every event handler takes the old self and
returns a new self; the framework swaps the new value
into the tree and the renderer reuses cached subtrees by
=== identity.
In practice a click flows like this: click → handler receives the old instance and returns a new one → the framework swaps the root in → the renderer diffs against the previous frame and patches the DOM. There is no explicit subscribe / unsubscribe step, no central store, no mutation in place. The rest of this tutorial introduces the syntax for describing those handlers and views; the model under all of it is what you just read.
State and Updates
Methods vs Input Handlers
Tutuca splits handler-style functions across two tables:
methods and input. Both are called on the
component instance and both return a new instance — the only
difference is how a template references them:
-
.name(with a leading dot) callsmethods.nameor any auto-generated mutator (.setX,.toggleX, …). name(bare) callsinput.name.
By convention, methods holds anything reasonable to call
from JS (computed values, predicates, derivations);
input holds names that only make sense as
template-attached event handlers. Pick the table that reads best where
the handler will be called from — the linter flags mismatches in
either direction (INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER,
INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD) so the choice is
recoverable.
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.
Conditional Display
@show=".field" hides the host element when the field is
falsy; @hide=".field" hides it when truthy. The element
still renders in the DOM — only its visibility is toggled
— so prefer this for fast-toggling regions where mounting cost
isn't a concern.
The same show and hide can also appear as
wrapper attributes on the <x> render ops
(render, render-it,
render-each, text) — instead of
toggling a host element, they wrap the produced node in the equivalent
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.
The value can be a field, an auto-generated predicate
(.isXTruthy, .isXEmpty,
.isXNull — only the matching ones for the field's
type), or a methods entry —
@show=".canSubmit" calls
methods.canSubmit() with no arguments and uses its return
value. Reach for a method when a predicate combines multiple fields;
the auto-generated predicates only cover single-field checks.
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).
The example below also uses
@show=".isLastSentSearchTruthy" to conditionally display
the search result — isXTruthy 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 isXNull for explicit null
checks. @show and @hide were 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 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.
.toggleIsActive in the example is auto-generated for
boolean fields. The value passed to @if can also be a
method — same rule as @show, useful when the
predicate combines multiple fields.
When there is a single @if directive,
@then and @else don't need to specify the
attribute name — they infer it from
@if.<attr>. 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) — HTML disallows duplicate attribute
names, so the second unsuffixed @then= would be silently
dropped by the parser before tutuca sees it.
No Dotted Paths in Values
Tutuca expressions resolve a single name on
this. Writing @text=".user.name",
:value=".item.title", or
@show=".item.isOpen" does not navigate —
the parser will not walk a dotted path anywhere a value is read. This
is the most common gotcha in the language; it's worth remembering once
and never tripping on again. When the value lives one level deeper,
you have three options:
-
Render the child as a component —
<x render=".user">and read.namefrom inside the child's view. Best when the nested thing is already (or could be) its own component. -
Add a method that returns it —
userName() { return this.user.name; }, then use@text=".userName". Best for one-off derivations or formatting. -
Use
@enrich-with— expose computed values as@-prefixed bindings to a subtree without putting them on the component. See Scope Enrichment.
Exceptions: @each / render-each accept
.field or *dynamic only (not a method call),
and <x render> expects a component instance —
for a derived list, store it in a field or use @when with
alter.
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.
-
'string'— single-quoted string literal. Works anywhere a value is allowed (@then="'btn ok'",:label="'Sale'"). -
Bare with interpolation:
:class="btn {.kind}"— works in:attr=,@text, macro dynamic attrs, anywhere a string template is allowed. The presence of{...}turns the whole value into a template. -
Bare without quotes or braces:
:class="flex gap-3"— does not work, the parser returnsnull. Either single-quote it or add a{...}piece. -
Bare identifier:
dec,value— valid only in name slots (event handler name, handler arg name), never as a value.
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.
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.
Rendering Components
Rendering a Child Component
Up to this point components have been 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> is the same idea but for use
inside an iteration (@each or
render-each) — it renders whatever component
instance is currently iterated.
Once you can render children, the value tree starts to look like a
real tree: each <x render> is a parent → child
edge, and a click inside the grandchild produces a new grandchild,
which produces a new child, which produces a new root (recall
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 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.
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.
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">.
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 — exactly what the recursive-tree example
in the next section uses to turn a nested object literal into a fully
constructed TreeItem 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.
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.Class.fromData (a
static, see previous section) is what builds the nested structure from
plain data. The component style uses CSS pseudo-elements
(:before) to show folder / file icons based on class
names set with @if.class.
Component Communication
Components don't have to talk to each other — many apps just let parents read children's state directly. But once you want one component to tell another to do something, tutuca offers three channels: send (targeted message), bubble (event observed by ancestors), and request (async, with a response). Plus dynamic bindings for the prop-drilling case where a descendant needs read-only access to an ancestor's value.
Send / Receive
ctx.send(name, args) dispatches a named handler in
receive: { ... } — on the current component when
called bare, or on any component reachable by path when prefixed with
ctx.at. 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 bubble vs request).
Send delivers a message to a specific target by path —
use it when one component needs to address another by name (a form
telling its email field to focus, a list telling item 3 to enter edit
mode), or to call a receive handler on self from multiple
call sites without duplicating its body. Bubble emits an
event upward that any ancestor can observe (next section).
Request is the async counterpart (covered after that).
In the example below, the form sends
ctx.at.field("status").send("flash", [text]) to its
sibling Status child on submit, and
ctx.send("clearDraft") to itself to reuse the same reset
handler from a hypothetical second call site. The
Status component owns its own state and knows nothing
about who sent the message — the path is the only coupling.
Bubble Events
When to bubble: handle the event locally if the
current component owns the state needed to respond. Bubble when the
action belongs to an ancestor (a list item's remove must
reach the list that owns the items), or when an ancestor may want to
react to or record something that happened (selection, logging,
analytics). Don't bubble events with no consumer.
The recursive tree.js example from
Recursive Components 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 /
receive 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.
Async Requests
Lifecycle note: tutuca has no built-in lifecycle. The
receive section is just a place to register named
handlers; nothing in the framework calls
receive.init automatically. The host application has to
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
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.
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
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 (recall 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 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.
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.
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.
Testing
Components have three layers worth testing: 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
tests written in the same module as the component — the same
tests tutuca <module> test picks up from the CLI.
Test Setup
A module opts into testing by exporting getTests({ describe,
test, expect }). expect is chai;
describe and test are tutuca's own subset
of the common Mocha/Bun-style API (no before /
after / beforeEach). Tests can be grouped
by component with describe(MyComp, () => { ... }),
which auto-tags the suite so
tutuca <module> test MyComp picks it up.
Calling Methods and Input Handlers
Methods are bound 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) mutates each
kept item's bindings. this is the parent component
instance in all three.
To test these as a pipeline (filter + loop-data + enrichment) use
collectIterBindings. It is only exposed by the
tutuca-dev build — the playgrounds on this page
import it from "tutuca" because their import map points
that specifier at tutuca-dev. In an app that uses the
tutuca or tutuca-extra production builds,
import test helpers from "tutuca/dev" instead:
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 methods, an input
handler, 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. Running it after each edit (the post-edit
recipe in the skill's core.txt →
Verifying changes) catches typos and broken references before
they hit a render. The example below intentionally triggers every
category — open the Lint tab on the right to
see them all detected.
The categories the linter reports, grouped as in the CLI reference:
-
Field references —
.fieldnot declared infields(FIELD_VAL_NOT_DEFINED). -
Method ↔ input handler confusion —
calling a
methodsentry without the.prefix (or aninputhandler with one), andinput/methodsentries referenced but not declared (INPUT_HANDLER_NOT_IMPLEMENTED,INPUT_HANDLER_METHOD_NOT_IMPLEMENTED,INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD,INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER). The linter knows which section each name lives in. -
Iteration helpers (
alter) —@when/@enrich-with/@loop-withname not inalter, or analterentry never used (ALT_HANDLER_NOT_DEFINED,ALT_HANDLER_NOT_REFERENCED). -
render-itoutside a loop —<x render-it>only works inside@each/render-each(RENDER_IT_OUTSIDE_OF_LOOP). -
Unknown event modifiers — e.g.
@on.click+badmodwhen only+ctrl/+cmd/+meta/+alt(and onkeydown:+send/+cancel) are recognized (UNKNOWN_EVENT_MODIFIER). -
Unknown handler arg names — only the built-in
arg names (
value,valueAsInt,event,ctx, …) are accepted in handler argument slots (UNKNOWN_HANDLER_ARG_NAME). -
Duplicate attribute definitions — setting the
same attribute (e.g.
class) via a literal,:class, and@if.classon the same element. Only one wins; the linter flags the conflict (DUPLICATE_ATTR_DEFINITION). -
Unknown
@directive— e.g.@bogus="..."on an element. Catches typos in directive names like@show,@text,@on.event,@if.attr, etc. (UNKNOWN_DIRECTIVE). -
Unknown
<x>op — the first attribute on<x>(or pseudo-@x) is not a recognized op (render,render-it,render-each,text,show,hide,slot) (UNKNOWN_X_OP). -
Unknown
<x>attribute — extra attribute on a<x op>that the op doesn't consume and isn't a known wrapper (show,hide) (UNKNOWN_X_ATTR). -
Names registered with the app —
!requestname not registered, component type not registered, or macro attribute not declared in defaults (UNKNOWN_REQUEST_NAME,UNKNOWN_COMPONENT_NAME,UNKNOWN_MACRO_ARG). -
Unreferenced declarations —
inputoralterentries that no view ever uses (INPUT_HANDLER_NOT_REFERENCED,ALT_HANDLER_NOT_REFERENCED). Warning level; useful for catching dead code after a refactor.
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.