tutuca 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tutuca-cli.js +264 -361
- package/dist/tutuca-components.js +175 -175
- package/dist/tutuca-dev.ext.js +250 -323
- package/dist/tutuca-dev.js +250 -323
- package/dist/tutuca-dev.min.js +4 -4
- package/dist/tutuca-extra.ext.js +55 -46
- package/dist/tutuca-extra.js +55 -46
- package/dist/tutuca-extra.min.js +3 -3
- package/dist/tutuca-storybook.js +29 -29
- package/dist/tutuca.ext.js +55 -46
- package/dist/tutuca.js +55 -46
- package/dist/tutuca.min.js +3 -3
- package/package.json +4 -3
- package/skill/margaui/SKILL.md +0 -105
- package/skill/margaui/components/accordion.md +0 -127
- package/skill/margaui/components/alert.md +0 -174
- package/skill/margaui/components/aura.md +0 -97
- package/skill/margaui/components/avatar.md +0 -220
- package/skill/margaui/components/badge.md +0 -193
- package/skill/margaui/components/breadcrumbs.md +0 -103
- package/skill/margaui/components/button.md +0 -322
- package/skill/margaui/components/calendar.md +0 -67
- package/skill/margaui/components/card.md +0 -373
- package/skill/margaui/components/carousel.md +0 -387
- package/skill/margaui/components/chat.md +0 -171
- package/skill/margaui/components/checkbox.md +0 -101
- package/skill/margaui/components/collapse.md +0 -172
- package/skill/margaui/components/countdown.md +0 -165
- package/skill/margaui/components/diff.md +0 -53
- package/skill/margaui/components/divider.md +0 -107
- package/skill/margaui/components/dock.md +0 -173
- package/skill/margaui/components/drawer.md +0 -184
- package/skill/margaui/components/dropdown.md +0 -388
- package/skill/margaui/components/fab.md +0 -346
- package/skill/margaui/components/fieldset.md +0 -88
- package/skill/margaui/components/file-input.md +0 -84
- package/skill/margaui/components/filter.md +0 -52
- package/skill/margaui/components/footer.md +0 -583
- package/skill/margaui/components/hero.md +0 -135
- package/skill/margaui/components/hover-3d.md +0 -129
- package/skill/margaui/components/hover-gallery.md +0 -49
- package/skill/margaui/components/indicator.md +0 -265
- package/skill/margaui/components/input.md +0 -389
- package/skill/margaui/components/join.md +0 -100
- package/skill/margaui/components/kbd.md +0 -127
- package/skill/margaui/components/label.md +0 -102
- package/skill/margaui/components/link.md +0 -96
- package/skill/margaui/components/list.md +0 -182
- package/skill/margaui/components/loading.md +0 -105
- package/skill/margaui/components/mask.md +0 -168
- package/skill/margaui/components/megamenu.md +0 -131
- package/skill/margaui/components/menu.md +0 -887
- package/skill/margaui/components/mockup-browser.md +0 -39
- package/skill/margaui/components/mockup-code.md +0 -81
- package/skill/margaui/components/mockup-phone.md +0 -39
- package/skill/margaui/components/mockup-window.md +0 -33
- package/skill/margaui/components/modal.md +0 -196
- package/skill/margaui/components/navbar.md +0 -282
- package/skill/margaui/components/otp.md +0 -171
- package/skill/margaui/components/pagination.md +0 -122
- package/skill/margaui/components/progress.md +0 -135
- package/skill/margaui/components/radial-progress.md +0 -67
- package/skill/margaui/components/radio.md +0 -133
- package/skill/margaui/components/range.md +0 -134
- package/skill/margaui/components/rating.md +0 -170
- package/skill/margaui/components/select.md +0 -225
- package/skill/margaui/components/skeleton.md +0 -64
- package/skill/margaui/components/stack.md +0 -142
- package/skill/margaui/components/stat.md +0 -254
- package/skill/margaui/components/status.md +0 -73
- package/skill/margaui/components/steps.md +0 -138
- package/skill/margaui/components/swap.md +0 -152
- package/skill/margaui/components/tab.md +0 -248
- package/skill/margaui/components/table.md +0 -1018
- package/skill/margaui/components/text-rotate.md +0 -91
- package/skill/margaui/components/textarea.md +0 -85
- package/skill/margaui/components/theme-controller.md +0 -266
- package/skill/margaui/components/timeline.md +0 -1356
- package/skill/margaui/components/toast.md +0 -165
- package/skill/margaui/components/toggle.md +0 -135
- package/skill/margaui/components/tooltip.md +0 -181
- package/skill/margaui/components/validator.md +0 -163
- package/skill/tutuca/SKILL.md +0 -56
- package/skill/tutuca/advanced.md +0 -212
- package/skill/tutuca/cli.md +0 -239
- package/skill/tutuca/component-design.md +0 -168
- package/skill/tutuca/core.md +0 -908
- package/skill/tutuca/iteration.md +0 -207
- package/skill/tutuca/macros.md +0 -86
- package/skill/tutuca/margaui.md +0 -175
- package/skill/tutuca/messages-and-intents.md +0 -399
- package/skill/tutuca/patterns/README.md +0 -48
- package/skill/tutuca/patterns/add-a-story.md +0 -26
- package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
- package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
- package/skill/tutuca/patterns/coordinate-components.md +0 -54
- package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -27
- package/skill/tutuca/patterns/enrich-each-item.md +0 -25
- package/skill/tutuca/patterns/file-input.md +0 -39
- package/skill/tutuca/patterns/filter-a-list.md +0 -25
- package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
- package/skill/tutuca/patterns/handle-events.md +0 -38
- package/skill/tutuca/patterns/iterate-a-list.md +0 -18
- package/skill/tutuca/patterns/paginate-a-list.md +0 -29
- package/skill/tutuca/patterns/render-a-child-component.md +0 -21
- package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
- package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -38
- package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
- package/skill/tutuca/patterns/switch-between-views.md +0 -30
- package/skill/tutuca/patterns/tabbed-interface.md +0 -43
- package/skill/tutuca/semantics.md +0 -195
- package/skill/tutuca/storybook.md +0 -270
- package/skill/tutuca/styles.md +0 -48
- package/skill/tutuca/testing.md +0 -346
- package/skill/tutuca-source/SKILL.md +0 -33
- package/skill/tutuca-source/tutuca.ext.js +0 -4235
package/skill/tutuca/core.md
DELETED
|
@@ -1,908 +0,0 @@
|
|
|
1
|
-
# Tutuca — Core
|
|
2
|
-
|
|
3
|
-
Tutuca is an immutable-state web framework powered by Immer: components have
|
|
4
|
-
typed `fields`, native JavaScript collections, HTML-template `view`s with
|
|
5
|
-
`@`-prefixed directives, and `receive` / `intent` handlers for
|
|
6
|
-
orchestration. Read this file when authoring or reviewing
|
|
7
|
-
`component({...})` definitions, `view: html\`...\`` templates, macros, or
|
|
8
|
-
the `tutuca` CLI.
|
|
9
|
-
|
|
10
|
-
> Load the topic files only when the task touches them (the routing
|
|
11
|
-
> table in [SKILL.md](./SKILL.md) has the full descriptions):
|
|
12
|
-
> [iteration.md](./iteration.md) · [macros.md](./macros.md) ·
|
|
13
|
-
> [styles.md](./styles.md) · [messages-and-intents.md](./messages-and-intents.md) ·
|
|
14
|
-
> [component-design.md](./component-design.md) · [testing.md](./testing.md) ·
|
|
15
|
-
> [storybook.md](./storybook.md) · [cli.md](./cli.md) ·
|
|
16
|
-
> [semantics.md](./semantics.md) · [advanced.md](./advanced.md) ·
|
|
17
|
-
> [margaui.md](./margaui.md) · [patterns/README.md](./patterns/README.md).
|
|
18
|
-
|
|
19
|
-
## Verifying changes
|
|
20
|
-
|
|
21
|
-
After editing a Tutuca module, run these checks before declaring the
|
|
22
|
-
edit done:
|
|
23
|
-
|
|
24
|
-
1. **Lint the module** — catches undefined fields/handlers/macros/events
|
|
25
|
-
(all the `*_NOT_DEFINED` / `*_NOT_REFERENCED` codes):
|
|
26
|
-
|
|
27
|
-
tutuca lint <module-path>
|
|
28
|
-
|
|
29
|
-
Exits `2` on any error-level finding. Pass a component name to scope
|
|
30
|
-
it: `tutuca lint <module-path> Button`.
|
|
31
|
-
|
|
32
|
-
2. **Test component behavior** — when the edit changes attributes,
|
|
33
|
-
instance methods, receive handlers, or static factories (anything
|
|
34
|
-
observable from JS, not just the rendered HTML), run the test
|
|
35
|
-
suite. The module opts in by exporting
|
|
36
|
-
`getTests({ describe, test, expect })`:
|
|
37
|
-
|
|
38
|
-
tutuca test <module-path>
|
|
39
|
-
tutuca test <module-path> Counter # one component
|
|
40
|
-
tutuca test <module-path> --grep "inc()" # one path
|
|
41
|
-
|
|
42
|
-
Exits `4` on any failure. Skip this step when the change is purely
|
|
43
|
-
templates/styling — `render` already covers that. Authoring patterns
|
|
44
|
-
(handler calling convention, designing handlers for testability,
|
|
45
|
-
worked `getTests` export) in [testing.md](./testing.md); CLI flags
|
|
46
|
-
and exit codes in [cli.md](./cli.md).
|
|
47
|
-
|
|
48
|
-
3. **Render the example(s) that exercise the feature you changed** —
|
|
49
|
-
confirms the component actually mounts in a headless DOM with the new
|
|
50
|
-
behavior. Pick the example whose `title` matches the feature, or
|
|
51
|
-
filter by component:
|
|
52
|
-
|
|
53
|
-
tutuca render <module-path> --title "Disabled state"
|
|
54
|
-
tutuca render <module-path> Button
|
|
55
|
-
|
|
56
|
-
Exits `3` if any render crashes. If no example covers the feature
|
|
57
|
-
you're adding, add one to `getExamples()` first — that's how the
|
|
58
|
-
feature becomes verifiable. Add `--pretty` when you need to read the
|
|
59
|
-
emitted HTML to verify structure (attributes, nesting, text); omit it
|
|
60
|
-
when you only care that the render didn't crash.
|
|
61
|
-
|
|
62
|
-
4. **Smoke-test the whole project** — when you've touched several
|
|
63
|
-
`*.dev.js` modules, or are about to launch the storybook, do a
|
|
64
|
-
project-wide dry run instead of opening a browser:
|
|
65
|
-
|
|
66
|
-
tutuca storybook --dry-run
|
|
67
|
-
tutuca storybook --dry-run --json # machine-readable for agents
|
|
68
|
-
|
|
69
|
-
It does everything the server would do up front — discovers every
|
|
70
|
-
co-located `*.dev.js`, imports and normalizes each (catching a missing
|
|
71
|
-
`getComponents()` or a malformed `getExamples()` shape), runs their
|
|
72
|
-
`getTests()`, and resolves the runtime import map — then prints what
|
|
73
|
-
it *would* show instead of serving. A broken module is reported in
|
|
74
|
-
place (an `error` line, or `modules[].error` in `--json`) while the
|
|
75
|
-
others still report, so one bad module never hides the rest. This is
|
|
76
|
-
the fast "is the whole catalog wired up correctly?" check; steps 1–3
|
|
77
|
-
stay the per-module loop.
|
|
78
|
-
|
|
79
|
-
Full reference: [cli.md](./cli.md).
|
|
80
|
-
|
|
81
|
-
The Tutuca CLI only catches Tutuca-specific issues. For generic JS
|
|
82
|
-
problems, pair it with a general linter/formatter — e.g. set up Biome
|
|
83
|
-
once with `npx -y @biomejs/biome init` and use its `lint`, `check`,
|
|
84
|
-
and `format` subcommands. Run `npx @biomejs/biome -h` for usage help.
|
|
85
|
-
|
|
86
|
-
## Common pitfalls
|
|
87
|
-
|
|
88
|
-
- **`.field` reads a field, `$method` calls a no-arg method.** The two are
|
|
89
|
-
distinct prefixes: `.count` reads field `count`, `$inc` calls method
|
|
90
|
-
`inc`. Using the wrong one is a lint error that tells you to swap the
|
|
91
|
-
prefix.
|
|
92
|
-
- **Paths are not allowed in values.** `.foo` resolves a single field on
|
|
93
|
-
`this` — `@text=".foo.bar"`, `:value=".user.name"`, `@show=".item.isOpen"`
|
|
94
|
-
all fail. To reach into nested data: render the child as a component
|
|
95
|
-
(`<x render=".foo">` then `@text=".bar"` inside), add a method
|
|
96
|
-
(`fullName() { return this.user.name; }` and use `$fullName`), or use
|
|
97
|
-
`@enrich-with` for scope-level derivation. The one exception: a
|
|
98
|
-
**binding** may read exactly one **binding member** —
|
|
99
|
-
`@text="@value.title"` inside `@each` works (any `@`-binding, one level
|
|
100
|
-
only; a binding member read like `@value.a.b` is a lint error, and
|
|
101
|
-
render targets still reject it).
|
|
102
|
-
- **State writes happen on the handler's first `draft` argument.** `this` is
|
|
103
|
-
the frozen pre-transaction snapshot. Arrays, objects, Map, and Set use their
|
|
104
|
-
normal JavaScript mutation APIs on `draft`.
|
|
105
|
-
- **Multiple `@if.<attr>` on one element.** Every `@then`/`@else` after
|
|
106
|
-
the first must name the attr (`@then.title`, `@else.title`) — HTML
|
|
107
|
-
disallows duplicate attrs, so the second `@then=` is dropped silently.
|
|
108
|
-
- **Bare unquoted multi-word strings return `null`.** Either quote
|
|
109
|
-
(`'flex gap-3'`) or use a `$'…'` string template (`$'flex gap-3 {.color}'`).
|
|
110
|
-
- **`<x>` is stripped inside `<select>` / `<table>` / `<tr>`.** Use the
|
|
111
|
-
`@x` pseudo-x trick (see [advanced.md](./advanced.md)).
|
|
112
|
-
- **`receive.init` is a convention, not a lifecycle hook.** Nothing calls it
|
|
113
|
-
automatically — dispatch via `app.sendAtRoot("init")` or from
|
|
114
|
-
another handler.
|
|
115
|
-
- **`app.state.set(...)` takes a component instance**, not plain data.
|
|
116
|
-
Build with `Comp.make({...})`.
|
|
117
|
-
- **`html\`` templates must start with the opening tag.** A leading
|
|
118
|
-
newline / indent before the first element renders blank silently.
|
|
119
|
-
Use `view: html\`<el ...>` (or `html\`<el<newline> attr<newline>>...`),
|
|
120
|
-
never `view: html\`<newline> <el ...>`. Same applies to macro bodies.
|
|
121
|
-
- **Macro registry keys are lowercased.** `<x:Card>` becomes `<x:card>` — see [macros.md](./macros.md).
|
|
122
|
-
|
|
123
|
-
## Bootstrap
|
|
124
|
-
|
|
125
|
-
```js
|
|
126
|
-
import { component, html, tutuca } from "tutuca";
|
|
127
|
-
|
|
128
|
-
const Counter = component({
|
|
129
|
-
name: "Counter",
|
|
130
|
-
fields: { count: 0 },
|
|
131
|
-
receive: {
|
|
132
|
-
inc(draft) {
|
|
133
|
-
draft.count++;
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
view: html`<button @on.click="inc" @text=".count"></button>`,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const app = tutuca("#app");
|
|
140
|
-
app.registerComponents([Counter]);
|
|
141
|
-
app.state.set(Counter.make({}));
|
|
142
|
-
app.start();
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
`app.onChange((info) => ...)` fires after every state change with
|
|
146
|
-
`{ val, old, info, timestamp }` (logging, persistence). `app.stop()`
|
|
147
|
-
removes all listeners and cancels cache eviction; pair with
|
|
148
|
-
`app.start()` to remount cleanly in tests or SPA navigation.
|
|
149
|
-
|
|
150
|
-
## Mental model
|
|
151
|
-
|
|
152
|
-
Tutuca rests on three invariants: application state is one deeply frozen root;
|
|
153
|
-
the view is a pure function of it; and every dispatched handler receives an
|
|
154
|
-
Immer draft as its first argument while `this` remains the immutable current
|
|
155
|
-
snapshot. The transactor commits the produced root atomically.
|
|
156
|
-
|
|
157
|
-
**The value tree.** Components are generated Immer-draftable classes. Children
|
|
158
|
-
live in fields — an array of `Item`, a native `Map` of `User`, a scalar `count`.
|
|
159
|
-
"Updating a deep child" means producing a new root that shares
|
|
160
|
-
structure with the old one along the unchanged spine; the renderer
|
|
161
|
-
keys its cache on `===` identity, so unchanged subtrees skip work.
|
|
162
|
-
Every value carries a hidden tag back to its component class, so the
|
|
163
|
-
runtime never needs `instanceof` — it asks the value what it is.
|
|
164
|
-
|
|
165
|
-
Because children are ordinary frozen values held in fields, **handlers
|
|
166
|
-
and methods are ordinary JS with full read access to nested child
|
|
167
|
-
state** — `this.child.count`, `this.items[i].done`,
|
|
168
|
-
`this.byKey.get(k).label`. Reading *down* the tree is direct and needs
|
|
169
|
-
no channel: an ancestor that owns a list already holds every child's
|
|
170
|
-
state and can read it for an aggregate decision. The single-level
|
|
171
|
-
`.field` restriction (no `.foo.bar`) is a **view-template** rule, not a
|
|
172
|
-
JS one — it's why a derivation like `userName() { return this.user.name; }`
|
|
173
|
-
is written as a method (see *Methods as Predicates & Computed Values*).
|
|
174
|
-
Reading is free; **mutating** a child still flows through the model — mutate
|
|
175
|
-
the addressed draft or message the child with `ctx.send`. Don't reach in to
|
|
176
|
-
mutate the frozen snapshot,
|
|
177
|
-
and prefer letting a child own and render its own state — reach down to
|
|
178
|
-
read only when the ancestor genuinely needs it. See
|
|
179
|
-
[component-design.md](./component-design.md) and "When to bubble" in
|
|
180
|
-
[messages-and-intents.md](./messages-and-intents.md).
|
|
181
|
-
|
|
182
|
-
**Stack: frames vs scopes.** As the renderer walks the AST it pushes
|
|
183
|
-
`BindFrame`s. A *frame* is a barrier: name lookups (`@x`) stop at it,
|
|
184
|
-
so a child component view sees a clean namespace. A *scope* is
|
|
185
|
-
transparent: iteration `key` / `value` and `@enrich-with` binds layer
|
|
186
|
-
onto the surrounding frame and remain visible to handlers attached to
|
|
187
|
-
the same iteration. `it` (the target of `.field` reads and `$method`
|
|
188
|
-
calls) is set on both.
|
|
189
|
-
|
|
190
|
-
| pushed by | kind | shape |
|
|
191
|
-
| ----------------------------------- | ----- | ------------------------------------ |
|
|
192
|
-
| `<x render=".f">` / `<x render-it>` | frame | `it` = child, fresh binds |
|
|
193
|
-
| `<x render-each>` per iter | frame | `it` = item, binds `{ key }` |
|
|
194
|
-
| `<div @each>` per iter | scope | `it` = item, binds `{ key, value }` |
|
|
195
|
-
| `<div @enrich-with=…>` (no `@each`) | scope | `it` unchanged, binds = alter result |
|
|
196
|
-
|
|
197
|
-
For full mechanics see [iteration.md](./iteration.md).
|
|
198
|
-
This is why a handler attached to `<div @each>` runs against the
|
|
199
|
-
*parent* component (the scope is transparent — the surrounding frame
|
|
200
|
-
still owns dispatch), while one inside `<x render-it>` runs against
|
|
201
|
-
the *item* (render-it pushed a fresh frame for the child).
|
|
202
|
-
|
|
203
|
-
**Paths, not references.** The DOM is the only thing that survives
|
|
204
|
-
between render and click, so the renderer leaves breadcrumbs:
|
|
205
|
-
`data-cid` / `data-nid` / `data-eid` on rendered elements, and `§…§`
|
|
206
|
-
HTML comments adjacent to iteration entries. On a DOM event the
|
|
207
|
-
runtime walks from the target up to the root, reads those breadcrumbs,
|
|
208
|
-
and rebuilds a *positional* `Path` — an array of steps from the root
|
|
209
|
-
to the value the handler should run against. The same `Path` is reused
|
|
210
|
-
verbatim for `ctx.send` and `ctx.intent`: because it's positional rather
|
|
211
|
-
than a captured reference, an async answer survives intervening
|
|
212
|
-
transactions that rebuild the root.
|
|
213
|
-
"The right slot" is exact for named fields and for map entries by key
|
|
214
|
-
(seq-access keys like `.sheets[.selId]` are *pinned* to their
|
|
215
|
-
dispatch-time value by default); a bare list **index** still slides if the
|
|
216
|
-
list reordered. See [messages-and-intents.md](./messages-and-intents.md) for the
|
|
217
|
-
dispatch APIs and [semantics.md](./semantics.md) for the path/transaction
|
|
218
|
-
model and key pinning.
|
|
219
|
-
|
|
220
|
-
**Why `alter` is its own table.** Alter handlers are pure, evaluated
|
|
221
|
-
on every render, and produce binds (no state change). `receive` and
|
|
222
|
-
`intent` are transactional and produce new values. Same lookup mechanism, different contracts — keep them
|
|
223
|
-
separate.
|
|
224
|
-
|
|
225
|
-
## Notation Reference
|
|
226
|
-
|
|
227
|
-
Views are name-based: there is no arithmetic expression syntax in
|
|
228
|
-
values, and no Vue- or Mustache-style `{{ … }}` placeholders. Every
|
|
229
|
-
value slot — conditions (`@show`, `@if`), iteration (`@each`,
|
|
230
|
-
`render-each`, `@when`), enrichment (`@enrich-with`, `@loop-with`), template
|
|
231
|
-
expansion (`{…}`, `:attr`, `@text`) — names a field, method, macro, or
|
|
232
|
-
handler defined on the component (or registered with the app). Logic
|
|
233
|
-
lives in `methods` / `alter` / `receive` / `intent` and is referenced by
|
|
234
|
-
name; the template itself only routes
|
|
235
|
-
data and events.
|
|
236
|
-
|
|
237
|
-
The one exception is **boolean predicates** in conditional slots
|
|
238
|
-
(`@show`, `@hide`, `@if.<attr>`): a closed set of operators applied to
|
|
239
|
-
a value, written predicate-first like a handler call —
|
|
240
|
-
`empty?`, `truthy?`, `falsy?`, `null?`, `equals?`. E.g.
|
|
241
|
-
`@hide="empty? .items"`, `@show="truthy? .query"`. A conditional slot
|
|
242
|
-
otherwise accepts the same value forms as `@text` — a plain field
|
|
243
|
-
(`@show=".isOpen"`), a no-arg method (`@show="$canSubmit"`), or a loop/scope
|
|
244
|
-
`@binding` (`@show="@isSelected"`, `@hide="@hasDesc"`) — read as a boolean.
|
|
245
|
-
|
|
246
|
-
`equals?` takes two args and is the idiomatic way to show/hide by name,
|
|
247
|
-
e.g. `@show="equals? .view 'detail'"`. Predicate args (and handler
|
|
248
|
-
args) accept string literals: `'detail'`, or `'two words'` for a
|
|
249
|
-
literal with spaces (escape an interior quote as `\'`).
|
|
250
|
-
|
|
251
|
-
| Prefix | Means | Example |
|
|
252
|
-
| -------- | ----------------------------------------- | --------------------- |
|
|
253
|
-
| `.x` | field on `this` (single-level — no `.foo.bar` paths) | `.count`, `.title` |
|
|
254
|
-
| `$x` | no-arg method call on `this` (a method reference) | `$inc`, `$canSubmit` |
|
|
255
|
-
| `@x` | local binding (loop / scope) | `@key`, `@value` |
|
|
256
|
-
| `^x` | macro parameter | `^label` |
|
|
257
|
-
| `*x` | dynamic binding — see [advanced.md](./advanced.md) | `*theme` |
|
|
258
|
-
| `Name` | component type (PascalCase) | `Item`, `JsonNull` |
|
|
259
|
-
| `name` | bare identifier — meaning depends on slot | `dec`, `value` |
|
|
260
|
-
| `'str'` | string literal | `'btn btn-success'` |
|
|
261
|
-
| `$'…'` | string template (`{expr}` interpolation) | `$'Hi {.name}'` |
|
|
262
|
-
| `.s[.k]` | sequence/map item access | `.byKey[.currentKey]` |
|
|
263
|
-
| `pred? .x` | boolean predicate in a conditional slot | `empty? .items`, `equals? .view 'detail'` |
|
|
264
|
-
|
|
265
|
-
`.x` and `$x` are not interchangeable: `.x` only reads a field, `$x`
|
|
266
|
-
only calls a method. The linter flags a mismatch and tells you which
|
|
267
|
-
prefix to use.
|
|
268
|
-
|
|
269
|
-
A bare `name` (no prefix) in `@on.<event>="<handler> <arg> <arg>..."`
|
|
270
|
-
resolves by slot:
|
|
271
|
-
|
|
272
|
-
- **First slot** — handler name looked up in `receive` / `alter` (use
|
|
273
|
-
`$name` for `methods`).
|
|
274
|
-
- **Subsequent slots** — built-in handler argument name (full list in
|
|
275
|
-
*Event Handling*); anything else triggers a lint warning.
|
|
276
|
-
|
|
277
|
-
```html
|
|
278
|
-
<button @on.click="addItem JsonSelector">+</button>
|
|
279
|
-
<!-- ↑ handler ↑ Type -->
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
`ctx` (an `EventContext`) is auto-appended as the trailing arg, so the
|
|
283
|
-
handler is called as `addItem(JsonSelector, ctx)`. Don't list `ctx` in
|
|
284
|
-
the template — it's always passed.
|
|
285
|
-
|
|
286
|
-
## Quoting & String Literals
|
|
287
|
-
|
|
288
|
-
A string template is written `$'…'` — a single-quoted run with a leading
|
|
289
|
-
`$`, holding `{expr}` interpolations. `:attr=` and other text slots accept
|
|
290
|
-
`$'…'` templates; `@if`, `@each`, `<x render=>` do not.
|
|
291
|
-
|
|
292
|
-
| Form | Example | Where it works |
|
|
293
|
-
| ------------------- | ------------------------- | ------------------------------------------------ |
|
|
294
|
-
| `'string'` | `@then="'btn ok'"` | anywhere a value is allowed |
|
|
295
|
-
| `$'…'` template | `:class="$'btn {.kind}'"` | `:attr=`, `@text`, `@title`, macro dynamic attrs |
|
|
296
|
-
| Bare without quotes | `flex gap-3` | **never** — returns `null` |
|
|
297
|
-
| Bare identifier | `dec`, `value` | name slots only (handler/arg, not as a value) |
|
|
298
|
-
|
|
299
|
-
```html
|
|
300
|
-
<!-- ✅ -->
|
|
301
|
-
<p :class="'flex gap-3'">x</p>
|
|
302
|
-
<p :class="$'flex {.color}'">x</p> <!-- $'…' string template -->
|
|
303
|
-
<p :class="$'static-classes {\'\'}'">x</p> <!-- folds to a const -->
|
|
304
|
-
|
|
305
|
-
<!-- ❌ -->
|
|
306
|
-
<p :class="flex gap-3">x</p> <!-- null: no quotes -->
|
|
307
|
-
<p :class="flex {.color}">x</p> <!-- null: unquoted {…} is not a template -->
|
|
308
|
-
<x render="'foo bar'"></x> <!-- @render rejects string templates -->
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
## Component Skeleton
|
|
312
|
-
|
|
313
|
-
The object passed to `component({...})` is the **component spec** — the
|
|
314
|
-
linter warns on unknown spec keys. The full shape:
|
|
315
|
-
|
|
316
|
-
```js
|
|
317
|
-
component({
|
|
318
|
-
name: "MyComp",
|
|
319
|
-
fields: { // see "Field Types"
|
|
320
|
-
count: 0,
|
|
321
|
-
items: [],
|
|
322
|
-
nullable: null,
|
|
323
|
-
},
|
|
324
|
-
view: html`<p @text=".count"></p>`, // default view (named "main")
|
|
325
|
-
views: { // additional views
|
|
326
|
-
edit: html`<input :value=".count" @on.input="setCount valueAsInt" />`,
|
|
327
|
-
big: {
|
|
328
|
-
view: html`<h1 @text=".count"></h1>`,
|
|
329
|
-
style: css`h1 { font-size: 4rem; }`,
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
style: css`p { color: blue; }`, // scoped to main view
|
|
333
|
-
commonStyle: css`p { font-family: sans-serif; }`, // scoped to all views of this component
|
|
334
|
-
globalStyle: css`body { margin: 0; }`, // injected globally, no scoping
|
|
335
|
-
methods: {
|
|
336
|
-
doubled() { return this.count * 2; },
|
|
337
|
-
},
|
|
338
|
-
alter: { filterItem(_k, item) { return item.length > 0; } },
|
|
339
|
-
// ADDRESSED: this component's own @on.* names, what a parent sends it, and the
|
|
340
|
-
// answers to intents it raised — one bucket, and nothing tells them apart.
|
|
341
|
-
receive: {
|
|
342
|
-
inc(draft) { draft.count++; },
|
|
343
|
-
setCount(draft, value) { draft.count = value; },
|
|
344
|
-
onClick(draft) { draft.count++; },
|
|
345
|
-
init(_draft, ctx) { ctx.intent("loadData", [], { route: ["lex"] }); },
|
|
346
|
-
loadDataOk(draft, res) { draft.items = res; },
|
|
347
|
-
loadDataError(draft, err) { draft.error = String(err); },
|
|
348
|
-
},
|
|
349
|
-
// ROUTED: what this component answers for somebody who did not address it.
|
|
350
|
-
intent: { itemPicked(draft, item) { draft.selected = item; } },
|
|
351
|
-
statics: { fromData(d) { return this.make({ count: d.n ?? 0 }); } },
|
|
352
|
-
// provide: { ... }, lookup: { ... } // see advanced.md
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
`Comp.make({...})` builds and deeply freezes an instance. Arrays, plain objects,
|
|
357
|
-
native `Map`, and native `Set` stay native. Wrap nested component data with
|
|
358
|
-
`Child.make({...})` when it needs component identity and its own view/handlers.
|
|
359
|
-
|
|
360
|
-
## Field Types
|
|
361
|
-
|
|
362
|
-
`fields: { name: defaultValue }` — type inferred from the default.
|
|
363
|
-
|
|
364
|
-
| Default | Field type | Draft update example |
|
|
365
|
-
| --- | --- | --- |
|
|
366
|
-
| `"hi"` | text | `draft.x = value` |
|
|
367
|
-
| `42` | float | `draft.x++` |
|
|
368
|
-
| `{ type: "int", defaultValue: 0 }` | int | `draft.x = Math.trunc(value)` |
|
|
369
|
-
| `true` | bool | `draft.x = !draft.x` |
|
|
370
|
-
| `null` | any | `draft.x = value` |
|
|
371
|
-
| `[]` | list | `draft.x.push(value)`, `draft.x.splice(i, 1)` |
|
|
372
|
-
| `{}` | object | `draft.x.key = value` |
|
|
373
|
-
| `new Map()` | map | `draft.x.set(key, value)` |
|
|
374
|
-
| `new Set()` | set | `draft.x.add(value)`, `draft.x.delete(value)` |
|
|
375
|
-
|
|
376
|
-
Fields do not generate setters or collection mutators. Define only the named
|
|
377
|
-
handlers your view/API needs; the linter reports a field/method name collision.
|
|
378
|
-
|
|
379
|
-
Emptiness / truthiness / null checks are not generated as methods — use
|
|
380
|
-
the boolean predicates `empty?`, `truthy?`, `falsy?`, `null?`, `equals?`
|
|
381
|
-
in a conditional slot instead (e.g. `@hide="empty? .x"`,
|
|
382
|
-
`@show="equals? .view 'detail'"`).
|
|
383
|
-
|
|
384
|
-
Explicit field types via `classFromData`:
|
|
385
|
-
|
|
386
|
-
```js
|
|
387
|
-
fields: {
|
|
388
|
-
count: { type: "int", defaultValue: 10 }, // text/int/float/bool/list/object/map/set/any
|
|
389
|
-
child: { component: "Item", args: { ... } }, // deferred reference by name
|
|
390
|
-
child2: Item.make({ name: "" }), // direct default if Item is in scope
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
The `{ component, args }` form is for when the referenced component is **not
|
|
395
|
-
available** at field-definition time (forward reference, circular import).
|
|
396
|
-
`component` must be the component **name as a string** — passing the class
|
|
397
|
-
itself is a common mistake and is flagged by lint code
|
|
398
|
-
`COMP_FIELD_BAD_SHAPE`. When the component class **is** in scope, prefer
|
|
399
|
-
`ComponentName.make({...})` as the default value — no string indirection.
|
|
400
|
-
|
|
401
|
-
## Methods as Predicates & Computed Values
|
|
402
|
-
|
|
403
|
-
A no-arg method called via `$name` is invoked and its return value is
|
|
404
|
-
used. Works anywhere a value is read — `@text`, `:attr`, `@show` /
|
|
405
|
-
`@hide`, `@if.<attr>`, and `{…}` interpolation. (`.name` is a field
|
|
406
|
-
read and never invokes; `$name` is the method call.)
|
|
407
|
-
|
|
408
|
-
```js
|
|
409
|
-
methods: {
|
|
410
|
-
canSubmit() { return this.title.length > 0 && !this.isLoading; },
|
|
411
|
-
buttonClass() { return this.isActive ? "btn btn-primary" : "btn"; },
|
|
412
|
-
fullName() { return `${this.first} ${this.last}`; },
|
|
413
|
-
}
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
```html
|
|
417
|
-
<button @show="$canSubmit" :class="$buttonClass">Save</button>
|
|
418
|
-
<p :title="$'Hello, {$fullName}'" @text="$fullName"></p>
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
The boolean predicates (`empty?`, `truthy?`, `falsy?`, `null?`,
|
|
422
|
-
`equals?`) cover single-field checks in conditional slots; reach for a
|
|
423
|
-
method when the condition spans multiple fields or needs derivation. The
|
|
424
|
-
method takes no args.
|
|
425
|
-
|
|
426
|
-
Tutuca expressions resolve a **single** name on `this` — there is no
|
|
427
|
-
path syntax. `@text=".user.name"` does not navigate; it fails. When the
|
|
428
|
-
value lives behind a field, your options are:
|
|
429
|
-
|
|
430
|
-
- **Render the child as a component** — `<x render=".user">` then
|
|
431
|
-
`@text=".name"` inside the child's view. Best when the nested thing is
|
|
432
|
-
already (or could be) a component.
|
|
433
|
-
- **Add a method** — `userName() { return this.user.name; }` then
|
|
434
|
-
`@text="$userName"`. Best for one-off derivations or formatting.
|
|
435
|
-
- **Use `@enrich-with`** — exposes computed values as `@`-bindings to a
|
|
436
|
-
subtree without putting them on the component. See *Scope Enrichment*
|
|
437
|
-
in [iteration.md](./iteration.md).
|
|
438
|
-
|
|
439
|
-
Exceptions: `@each` / `render-each` accept `.field` or `*dynamic` only
|
|
440
|
-
(not a `$method` — a method result has no addressable path for event
|
|
441
|
-
dispatch, so `$m` is rejected there at parse time), and `<x render>`
|
|
442
|
-
expects a component instance — for a derived list, store it in a field
|
|
443
|
-
or use `@when` with `alter`.
|
|
444
|
-
|
|
445
|
-
## Statics
|
|
446
|
-
|
|
447
|
-
`statics: {...}` adds methods to the component **class**, not instances.
|
|
448
|
-
Available as `Comp.Class.<name>(...)` alongside the auto-generated
|
|
449
|
-
`Comp.Class.make(...)` (which `Comp.make(...)` aliases). Inside a static,
|
|
450
|
-
`this` is the class itself.
|
|
451
|
-
|
|
452
|
-
Common use: a `fromData` factory that recursively builds instances from
|
|
453
|
-
plain JS data:
|
|
454
|
-
|
|
455
|
-
```js
|
|
456
|
-
statics: {
|
|
457
|
-
fromData({ items = [] }) {
|
|
458
|
-
return this.make({ items: items.map((v) => Item.Class.fromData(v)) });
|
|
459
|
-
},
|
|
460
|
-
}
|
|
461
|
-
// usage: TreeRoot.Class.fromData([...])
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
> **Scopes own the `Class`.** A component is bound to a scope at
|
|
465
|
-
> `registerComponents` time — that scope owns its `Class`, component tag,
|
|
466
|
-
> and scope-bound `make`/statics — so a given component object is live in
|
|
467
|
-
> one scope at a time. Each app/registry is a separate scope; the same
|
|
468
|
-
> *object* registered into two of them rebinds (last wins). To run the
|
|
469
|
-
> same definition in two genuinely separate registries at once, build an
|
|
470
|
-
> independent copy with `component(Comp.spec)` (new id ⇒ separately
|
|
471
|
-
> compiled CSS + separate identity — the price of isolation, not a way to
|
|
472
|
-
> dedupe within one app). For reuse inside one scope, register the single
|
|
473
|
-
> object once and re-export it.
|
|
474
|
-
|
|
475
|
-
**Multi-scope caveat for statics.** A static like `fromData` that builds
|
|
476
|
-
a *different* child type by naming the imported const directly
|
|
477
|
-
(`Item.Class.fromData(v)` above) hardcodes the child's *original* scope.
|
|
478
|
-
Fine in a single-scope app; across scopes, resolve the child through the
|
|
479
|
-
caller's scope instead — `this.scope.lookupComponent("Item")` — so it
|
|
480
|
-
deserializes into the right one. Recursion into the *same* type needs no
|
|
481
|
-
lookup: `this.fromData(v)` / `this.make` already target the caller's scope.
|
|
482
|
-
|
|
483
|
-
## Text Rendering
|
|
484
|
-
|
|
485
|
-
```html
|
|
486
|
-
<span @text=".str"></span> <!-- prepend text into span -->
|
|
487
|
-
<x text=".bool"></x> <!-- text-only, no DOM element -->
|
|
488
|
-
<x text="$getStrUpper"></x> <!-- $ calls a method -->
|
|
489
|
-
<x text="@value"></x> <!-- loop binding -->
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
Use `@text` when you already have a host element to put the text in; use
|
|
493
|
-
`<x text=…>` for bare text with no wrapping element (e.g. text interleaved with
|
|
494
|
-
other inline content, or a loop binding). Both take the same value forms
|
|
495
|
-
(`.field`, `$method`, `@binding`).
|
|
496
|
-
|
|
497
|
-
## Attribute Binding
|
|
498
|
-
|
|
499
|
-
```html
|
|
500
|
-
<input :value=".str" @on.input="setStr value" />
|
|
501
|
-
<a :href=".url" :title="$'Hi {.name}'">link</a> <!-- string template -->
|
|
502
|
-
<button :class="$'btn {.color}'">x</button>
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
Plain attrs are static. `:attr="..."` is a dynamic expression. Boolean
|
|
506
|
-
HTML attributes (`disabled`, `checked`, `hidden`, …) are auto-recognized;
|
|
507
|
-
pass a boolean field.
|
|
508
|
-
|
|
509
|
-
A static `class="…"` and a dynamic `:class`/`@if.class` **cannot coexist on the
|
|
510
|
-
same element** — setting one attribute two ways is a lint error
|
|
511
|
-
(`DUPLICATE_ATTR_DEFINITION`), and at runtime the dynamic value wins and the
|
|
512
|
-
static class is dropped. Fold any structural classes into the bound expression,
|
|
513
|
-
e.g. `:class="$'btn {.color}'"` (note `btn` is part of the template, not a
|
|
514
|
-
separate `class="btn"`). The same applies to other attributes — see the
|
|
515
|
-
duplicate-attribute note below.
|
|
516
|
-
|
|
517
|
-
The HTML parser lowercases attribute names before Tutuca sees them, so
|
|
518
|
-
`:mapId` arrives as `:mapid` and `<x:Card>` becomes `<x:card>`. Three
|
|
519
|
-
consequences:
|
|
520
|
-
|
|
521
|
-
- SVG attributes are case-sensitive. Tutuca special-cases `:viewbox` →
|
|
522
|
-
`viewBox` so SVG roots work; for other camelCased SVG attrs, wrap them
|
|
523
|
-
in components that emit raw markup.
|
|
524
|
-
- Custom-element property setters defined in camelCase **will not fire**.
|
|
525
|
-
`:mapId=".mapId"` runs `node.mapid = value`; if the
|
|
526
|
-
element defined `set mapId(...)`, the lookup misses and JS silently
|
|
527
|
-
creates an own data property `mapid` on the element instead of invoking
|
|
528
|
-
the setter — no error, no warning, the bound state stays null. Author
|
|
529
|
-
custom elements with kebab-case attributes plus lowercased property
|
|
530
|
-
setters (or aliases), and bind via `:kebab-name` from Tutuca templates.
|
|
531
|
-
- Macro registry keys are lowercased on insert for the same reason
|
|
532
|
-
(see [macros.md](./macros.md)).
|
|
533
|
-
|
|
534
|
-
Tutuca auto-namespaces by subtree: elements inside `<svg>` get the SVG
|
|
535
|
-
namespace and elements inside `<math>` get MathML, with spec-cased local
|
|
536
|
-
names preserved (`linearGradient`, `viewBox`). A `<foreignObject>` switches
|
|
537
|
-
its children back to the HTML namespace. Customised built-in elements work
|
|
538
|
-
via `is="..."` (e.g. `<button is="x-fancy">`); `is` is applied when the
|
|
539
|
-
element is created, so it must be a static attribute — setting it later
|
|
540
|
-
does not upgrade the element.
|
|
541
|
-
|
|
542
|
-
### When nothing renders (or renders unstyled)
|
|
543
|
-
|
|
544
|
-
A few mistakes fail quietly — no error, just a blank or unstyled result, which
|
|
545
|
-
is the slowest kind to debug. **Run `tutuca lint <module>` first**: it catches
|
|
546
|
-
several of these. The usual suspects:
|
|
547
|
-
|
|
548
|
-
- **Unparseable attribute value** → the attribute is silently dropped. A bare
|
|
549
|
-
multi-word value isn't a string — quote it (`:label="'two words'"`) or make it
|
|
550
|
-
a template (`:label="$'{.a} {.b}'"`). Lint flags this as `BAD_VALUE`.
|
|
551
|
-
- **camelCase attribute on a custom element** → setter no-op (see the lowercasing
|
|
552
|
-
note above). Use kebab-case attributes. Not lintable — the HTML parser
|
|
553
|
-
lowercases the name before either Tutuca or the linter sees it.
|
|
554
|
-
- **Forgotten margaui `_palette`/decoy view** → classes assembled in methods or
|
|
555
|
-
interpolations render unstyled. See [margaui.md](./margaui.md). Not lintable.
|
|
556
|
-
- **A whitespace-only `html\`\``** → blank render. A *leading* newline before the
|
|
557
|
-
root element is fine (the parser trims it); a template with no element at all
|
|
558
|
-
is not.
|
|
559
|
-
|
|
560
|
-
## Event Handling
|
|
561
|
-
|
|
562
|
-
```html
|
|
563
|
-
<!-- every event names a receive handler (no prefix) -->
|
|
564
|
-
<button @on.click="inc">+</button>
|
|
565
|
-
<button @on.click="dec">-</button>
|
|
566
|
-
|
|
567
|
-
<!-- pass args by name -->
|
|
568
|
-
<input @on.input="setStr value" />
|
|
569
|
-
<input @on.input="setN valueAsInt" />
|
|
570
|
-
<button @on.click="pick @key isAlt">pick</button>
|
|
571
|
-
<button @on.click="addItem JsonSelector">+</button> <!-- type as arg -->
|
|
572
|
-
<button @on.click="loadAnotherWay">load</button> <!-- ctx auto-appended -->
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
Every `@on.<event>` handler receives an `EventContext` as its trailing
|
|
576
|
-
arg automatically — written args come first, `ctx` last. So
|
|
577
|
-
`loadAnotherWay` is called as `loadAnotherWay(draft, ctx)`, and `pick @key isAlt`
|
|
578
|
-
is called as `pick(draft, key, isAlt, ctx)`. You can still write `ctx` in the
|
|
579
|
-
template (it resolves to a fresh `EventContext`), but it is redundant.
|
|
580
|
-
|
|
581
|
-
Built-in handler argument names: `value`, `valueAsInt`, `valueAsFloat`,
|
|
582
|
-
`target`, `event`, `isAlt`, `isShift`, `isCtrl`/`isCmd`, `key`, `keyCode`,
|
|
583
|
-
`isUpKey`, `isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`,
|
|
584
|
-
`dragInfo`.
|
|
585
|
-
|
|
586
|
-
The content of `value` depends on the event source:
|
|
587
|
-
|
|
588
|
-
| Source | What `value` resolves to |
|
|
589
|
-
|-----------------------------|--------------------------------------------------|
|
|
590
|
-
| `<input type="checkbox">` | `event.target.checked` (boolean) |
|
|
591
|
-
| `CustomEvent` | `event.detail` |
|
|
592
|
-
| anything else | `event.target.value` (string), or null if absent |
|
|
593
|
-
|
|
594
|
-
For numeric inputs, prefer `valueAsInt` / `valueAsFloat` to skip the
|
|
595
|
-
string parse.
|
|
596
|
-
|
|
597
|
-
Ask for the most granular arg the handler actually uses — `value` /
|
|
598
|
-
`valueAsInt` / `key`, not the raw `event` — when the specific value is
|
|
599
|
-
all you need. A handler that takes `event` forces every test and
|
|
600
|
-
storybook story to fabricate a DOM-event-shaped object
|
|
601
|
-
(`{ target: { value: … } }`); one that takes `value` is called with a
|
|
602
|
-
plain literal. (Genuine exceptions exist — e.g. a file input needs
|
|
603
|
-
`event` to reach `event.target.files`.) See
|
|
604
|
-
[testing.md](./testing.md) *Designing handlers so tests stay simple*.
|
|
605
|
-
|
|
606
|
-
### Event Modifiers
|
|
607
|
-
|
|
608
|
-
`@on.<event>+<mod>+<mod>=...`
|
|
609
|
-
|
|
610
|
-
Guards — run the handler only when they hold:
|
|
611
|
-
|
|
612
|
-
- All events: `+ctrl`, `+cmd`/`+meta`, `+alt`
|
|
613
|
-
- `keydown` only: `+send` (Enter), `+cancel` (Escape)
|
|
614
|
-
|
|
615
|
-
Effects — act on the DOM event, on all events:
|
|
616
|
-
|
|
617
|
-
- `+prevent` → `event.preventDefault()`
|
|
618
|
-
- `+stop` → `event.stopPropagation()`
|
|
619
|
-
|
|
620
|
-
```html
|
|
621
|
-
<input @on.keydown+send="submit value" @on.keydown+cancel="reset" />
|
|
622
|
-
<button @on.click+ctrl="soloOnly">ctrl-click</button>
|
|
623
|
-
<form @on.submit+prevent="save">…</form>
|
|
624
|
-
<input @on.keydown+send+prevent="submit value" />
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
Effects apply only when every guard on the same handler passed, whatever
|
|
628
|
-
order they are written in: `+send+prevent` prevents on Enter and nothing
|
|
629
|
-
else. They run from the app's root listener, which is where tutuca listens
|
|
630
|
-
— that is invisible to `+prevent` (the default action happens after the
|
|
631
|
-
event finishes propagating) but it makes `+stop` narrower than it looks:
|
|
632
|
-
it stops listeners *outside* the app root only. DOM ancestors between the
|
|
633
|
-
target and the root have already seen the event, and tutuca runs a single
|
|
634
|
-
handler per event anyway, so `+stop` never suppresses another `@on`.
|
|
635
|
-
|
|
636
|
-
### Web Components & Custom Events
|
|
637
|
-
|
|
638
|
-
Custom elements just work, and any `CustomEvent` they fire is reachable
|
|
639
|
-
via `@on.<event-name>`. The event's `detail` surfaces as `value`:
|
|
640
|
-
|
|
641
|
-
```js
|
|
642
|
-
import "https://cdn.jsdelivr.net/npm/emoji-picker-element/+esm";
|
|
643
|
-
|
|
644
|
-
receive: { onPick(draft, detail) { draft.current = detail.unicode; } }
|
|
645
|
-
view: html`<emoji-picker @on.emoji-click="onPick value"></emoji-picker>`,
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
Handle these events declaratively with `@on.<event-name>` in the view —
|
|
649
|
-
don't grab the node from host/glue code and `addEventListener` on it. A
|
|
650
|
-
listener attached from outside the component runs outside the handler
|
|
651
|
-
model: no draft transaction, no transactor batching, and the mutation
|
|
652
|
-
is invisible to the component that owns the state (the same hazard as
|
|
653
|
-
reaching into `app.state` directly). For any event with a real element in
|
|
654
|
-
the tree, `@on.` is the only entry point you need. Genuinely external
|
|
655
|
-
inbound sources (WebSocket, `postMessage`, timers) have no element to bind
|
|
656
|
-
— route those through `app.sendAtRoot` instead (see
|
|
657
|
-
[messages-and-intents.md](./messages-and-intents.md)).
|
|
658
|
-
|
|
659
|
-
Pitfall: binding a camelCase JS property on a custom element silently
|
|
660
|
-
fails — see the lowercasing rules in *Attribute Binding* above.
|
|
661
|
-
|
|
662
|
-
## Conditional Display
|
|
663
|
-
|
|
664
|
-
```html
|
|
665
|
-
<div @show=".isLoading">Loading...</div>
|
|
666
|
-
<div @hide=".isLoading">content</div>
|
|
667
|
-
|
|
668
|
-
<!-- boolean predicates; equals? compares against a string literal -->
|
|
669
|
-
<div @show="equals? .view 'detail'">detail view</div>
|
|
670
|
-
|
|
671
|
-
<!-- @show / @hide also work as directives on `<x>` render ops:
|
|
672
|
-
wraps the produced node, no extra DOM element. Allowed on
|
|
673
|
-
text / render / render-it / render-each. First attr in
|
|
674
|
-
source order becomes the outermost wrapper. -->
|
|
675
|
-
<x text=".name" @show=".isOpen"></x>
|
|
676
|
-
<x render-it @hide=".isHidden"></x>
|
|
677
|
-
<x render-each=".items" @when="filter" @show=".isOpen"></x>
|
|
678
|
-
|
|
679
|
-
<!-- Single @if: shorthand @then/@else (attr inferred) -->
|
|
680
|
-
<button @if.class=".isActive" @then="'btn btn-success'" @else="'btn btn-ghost'">
|
|
681
|
-
...
|
|
682
|
-
</button>
|
|
683
|
-
|
|
684
|
-
<!-- Multiple @if on same element: name the attr explicitly -->
|
|
685
|
-
<button
|
|
686
|
-
@if.class=".isActive"
|
|
687
|
-
@then="'on'"
|
|
688
|
-
@else="'off'"
|
|
689
|
-
@if.title=".isActive"
|
|
690
|
-
@then.title="'On'"
|
|
691
|
-
@else.title="'Off'"
|
|
692
|
-
>
|
|
693
|
-
...
|
|
694
|
-
</button>
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
> HTML disallows duplicate attrs, so with multiple `@if.<attr>` on one
|
|
698
|
-
> element every `@then`/`@else` after the first **must** include the attr
|
|
699
|
-
> name — otherwise the parser drops it before tutuca sees it.
|
|
700
|
-
|
|
701
|
-
## List Iteration & Scope Enrichment
|
|
702
|
-
|
|
703
|
-
```html
|
|
704
|
-
<li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
|
|
705
|
-
<x render-each=".items"></x>
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
Auto-bound names inside a loop are `@key` and `@value`. Iteration
|
|
709
|
-
(`@each` / `render-each`), filtering (`@when`), item and scope
|
|
710
|
-
enrichment (`@enrich-with`), pagination and the `@loop-with` return
|
|
711
|
-
shape, and the `@each` lifecycle: see [iteration.md](./iteration.md).
|
|
712
|
-
|
|
713
|
-
## Rendering Components
|
|
714
|
-
|
|
715
|
-
```html
|
|
716
|
-
<x render=".item"></x> <!-- default ("main") view -->
|
|
717
|
-
<x render=".item" as="edit"></x> <!-- specific view (literal) -->
|
|
718
|
-
<x render=".item" as=".mode"></x> <!-- view chosen by a field at runtime -->
|
|
719
|
-
<x render-it></x> <!-- only inside @each / render-each -->
|
|
720
|
-
<x render=".byIndex[.currentIndex]"></x> <!-- list item access -->
|
|
721
|
-
<x render=".byKey[.currentKey]"></x> <!-- map item access -->
|
|
722
|
-
<x render="*active"></x> <!-- dynamic binding — see advanced.md -->
|
|
723
|
-
<x render=".item" @show=".isOpen"></x> <!-- conditional wrap, see "Conditional Display" -->
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
The top-level `view` is registered under `"main"` (the default); extras
|
|
727
|
-
go under `views: { name: html\`...\` }`. `as` selects which view of the
|
|
728
|
-
rendered component to use, falling back to `main` if absent. It accepts the
|
|
729
|
-
same dynamic values as `@push-view` (a literal name like `edit`, or `.field`,
|
|
730
|
-
`*dyn`, `@bind`, `$method`, `$'…{x}…'`), evaluated against the **host**
|
|
731
|
-
component at render time. `as` only applies to the **direct** component — for
|
|
732
|
-
whole-subtree control, use `@push-view` (next section). For `render-each` the
|
|
733
|
-
selector is evaluated once against the host, so every item gets the same view.
|
|
734
|
-
|
|
735
|
-
## Multiple Views & View Stack
|
|
736
|
-
|
|
737
|
-
```js
|
|
738
|
-
component({
|
|
739
|
-
view: html`<p @text=".title"></p>`, // "main"
|
|
740
|
-
views: { edit: html`<input :value=".title" @on.input="setTitle value" />` },
|
|
741
|
-
});
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
```html
|
|
745
|
-
<!-- @push-view pushes a name onto the rendering stack;
|
|
746
|
-
descendants resolve to first matching view, falling back to "main" -->
|
|
747
|
-
<div @push-view=".view"><x render-each=".items"></x></div>
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
| Directive | Scope |
|
|
751
|
-
|--------------------|--------------------------------------------------------------------------|
|
|
752
|
-
| `as="edit"` / `as=".mode"` | One `<x render>` element only. Literal or dynamic (like `@push-view`), evaluated against the host. |
|
|
753
|
-
| `@push-view=".v"` | Every component rendered recursively under the host (children + descendants). Each picks the first stack entry it has a matching view for; falls back to `"main"`. Inner `@push-view`s nest, extending the outer ones. |
|
|
754
|
-
|
|
755
|
-
## Styles
|
|
756
|
-
|
|
757
|
-
`style` is scoped to the main view, `commonStyle` to all views of the
|
|
758
|
-
component, `globalStyle` is injected unscoped (see the *Component
|
|
759
|
-
Skeleton* above). Scoping mechanics, styling the root element with bare
|
|
760
|
-
declarations, and the at-rules that must live in `globalStyle`: see
|
|
761
|
-
[styles.md](./styles.md). Tailwind / MargaUI utility classes:
|
|
762
|
-
[margaui.md](./margaui.md).
|
|
763
|
-
|
|
764
|
-
## Triggers and Handlers
|
|
765
|
-
|
|
766
|
-
Tutuca has **two** dispatch channels, and one question separates them:
|
|
767
|
-
*does the sender know who handles this?*
|
|
768
|
-
|
|
769
|
-
| Triggered by | Handler block | Use for |
|
|
770
|
-
| ------------------------------------------------ | ------------------ | ------------------------------------------------ |
|
|
771
|
-
| DOM event (`click`, `input`, …) | `receive: { ... }` | the component handling its own events |
|
|
772
|
-
| `ctx.send(name)` — message to a target path | `receive: { ... }` | addressing one known component (or self) |
|
|
773
|
-
| an answer to an intent this component raised | `receive: { ... }` | `<name>Ok` / `<name>Error` / `<name>Unhandled` |
|
|
774
|
-
| `ctx.intent(name, args, opts)` — walks a route | `intent: { ... }` | a job the sender does not address |
|
|
775
|
-
|
|
776
|
-
The first three rows are one bucket and **nothing tells them apart** — a
|
|
777
|
-
component that answered its own click differently from the identical
|
|
778
|
-
`ctx.send` from its parent could be driven neither from a test nor from a
|
|
779
|
-
parent.
|
|
780
|
-
|
|
781
|
-
An intent carries a **route**: `["dyn"]` walks the ancestors, `["lex"]`
|
|
782
|
-
walks the handlers registered on the scope, and the default `["dyn","lex"]`
|
|
783
|
-
tries both. The verb does not decide which scope answers — the route does,
|
|
784
|
-
written at the call site.
|
|
785
|
-
|
|
786
|
-
Every dispatched `receive` or `intent` handler is called as
|
|
787
|
-
`handler(draft, ...args, ctx)`. `this` is the immutable current instance;
|
|
788
|
-
`draft` is its Immer draft; `ctx` is always trailing. Mutate `draft` and
|
|
789
|
-
return nothing (or return `draft`) to commit. Return any other value to swap
|
|
790
|
-
the addressed component for that value. Mutating the draft and returning a
|
|
791
|
-
replacement in the same handler is an error. An unchanged recipe preserves
|
|
792
|
-
the current identity. Routes,
|
|
793
|
-
`ctx.reply` / `ctx.fail` / `ctx.forward` / `ctx.stop`, the three
|
|
794
|
-
outcomes, `ctx.at`, the `$unknown` fallback, and intent-handler
|
|
795
|
-
registration are in [messages-and-intents.md](./messages-and-intents.md);
|
|
796
|
-
worked snippets in
|
|
797
|
-
[patterns/coordinate-components.md](./patterns/coordinate-components.md).
|
|
798
|
-
|
|
799
|
-
`alter` is a third handler block, but it isn't event-triggered — the
|
|
800
|
-
renderer invokes alter handlers with their existing read-only signature to
|
|
801
|
-
produce binds, not state changes
|
|
802
|
-
(see *Mental model*, and *Scope Enrichment* in
|
|
803
|
-
[iteration.md](./iteration.md)).
|
|
804
|
-
|
|
805
|
-
## Macros
|
|
806
|
-
|
|
807
|
-
Pure template expansion — `macro({ params }, html\`...\`)` definitions
|
|
808
|
-
called as `<x:name>`, with `^param` references, slots, and named slots:
|
|
809
|
-
see [macros.md](./macros.md). Registry keys are lowercased —
|
|
810
|
-
`<x:Card>` resolves as `<x:card>`.
|
|
811
|
-
|
|
812
|
-
## Raw HTML (escape hatch)
|
|
813
|
-
|
|
814
|
-
```html
|
|
815
|
-
<div @dangerouslysetinnerhtml=".trustedHtml"></div>
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
Bypasses all escaping; children of the element are ignored when active.
|
|
819
|
-
|
|
820
|
-
## Immer Utilities
|
|
821
|
-
|
|
822
|
-
Tutuca uses Immer internally but does not add Immer's API to the root export.
|
|
823
|
-
Import recipe utilities explicitly when tests or host code need them:
|
|
824
|
-
|
|
825
|
-
```js
|
|
826
|
-
import { produce, immerable } from "tutuca/immer";
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
Generated component classes are already draftable. Add `[immerable] = true`
|
|
830
|
-
to custom classes stored in state. Native `Map` and `Set` support is enabled by
|
|
831
|
-
the `tutuca/immer` entry point.
|
|
832
|
-
|
|
833
|
-
## Conventional Module Exports
|
|
834
|
-
|
|
835
|
-
Examples and the storybook glue follow this shape so files compose freely
|
|
836
|
-
and the `tutuca` CLI can introspect any module without per-app glue:
|
|
837
|
-
|
|
838
|
-
```js
|
|
839
|
-
export function getComponents() { return [Comp, ...]; }
|
|
840
|
-
export function getMacros() { return { name: macro }; } // optional
|
|
841
|
-
export function getIntentHandlers() { return { name: async fn }; } // optional
|
|
842
|
-
export function getRoot() { return Root.make({...}); }
|
|
843
|
-
export function getExamples() {
|
|
844
|
-
// Return one section, or an array of sections.
|
|
845
|
-
return {
|
|
846
|
-
title: "...",
|
|
847
|
-
description: "...",
|
|
848
|
-
// value = Comp.make(...); intentHandlers (optional) mocks this example's requests
|
|
849
|
-
items: [{ title, description, value, view, intentHandlers }],
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
An example item may carry an optional **`intentHandlers`** map — per-example
|
|
856
|
-
mocks (keyed by request name) that override the module's real
|
|
857
|
-
`getIntentHandlers()` for that one instance, so two examples of the same
|
|
858
|
-
component show different responses side by side. Return a fixture, `throw` for
|
|
859
|
-
the error path, or never resolve to hold a loading state. Full treatment, plus
|
|
860
|
-
the `on` lifecycle hooks, in [storybook.md](./storybook.md).
|
|
861
|
-
|
|
862
|
-
Best practice: have `getComponents()` return **every** component the module
|
|
863
|
-
defines — child and helper components included — and give each one at least
|
|
864
|
-
one item in `getExamples()`. A component left out of `getComponents()` is
|
|
865
|
-
invisible to `tutuca lint`/`render`/`test`, so it silently loses linting and
|
|
866
|
-
render coverage. If your components already live behind a differently named
|
|
867
|
-
export, alias it instead of teaching tools a new name:
|
|
868
|
-
|
|
869
|
-
```js
|
|
870
|
-
export { allMyComponents as getComponents } from "./app.js";
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
Put these exports in a co-located **`*.dev.js`** file (a dev-only module
|
|
874
|
-
holding stories + tests, never shipped) and `tutuca storybook` auto-discovers
|
|
875
|
-
and renders them with no setup — see [cli.md](./cli.md). The same shape is
|
|
876
|
-
consumed by the shipped `tutuca/storybook` library if you want to embed a
|
|
877
|
-
storybook in your own page — see [storybook.md](./storybook.md).
|
|
878
|
-
|
|
879
|
-
## See also
|
|
880
|
-
|
|
881
|
-
- [iteration.md](./iteration.md) — `@each` / `render-each`, `@when`,
|
|
882
|
-
`@enrich-with`, `@loop-with` pagination, and the loop lifecycle.
|
|
883
|
-
- [macros.md](./macros.md) — `macro()` definitions, `<x:name>` calls,
|
|
884
|
-
slots, and registration.
|
|
885
|
-
- [styles.md](./styles.md) — `style` / `commonStyle` / `globalStyle`
|
|
886
|
-
scoping mechanics and pitfalls.
|
|
887
|
-
- [component-design.md](./component-design.md) — design judgment for shaping a
|
|
888
|
-
feature into components: responsibilities, where state lives, which channel to
|
|
889
|
-
reach for, and a curated do's & don'ts list.
|
|
890
|
-
- [messages-and-intents.md](./messages-and-intents.md) — addressed
|
|
891
|
-
`send`-`receive` vs routed `intent`, the `ctx.at` `PathBuilder`, `$unknown`, and
|
|
892
|
-
request-handler registration.
|
|
893
|
-
- [advanced.md](./advanced.md) — dynamic bindings (`*x`), pseudo-`@x` for
|
|
894
|
-
`<select>` / `<table>` / `<tr>`, drag & drop, custom seq types.
|
|
895
|
-
- [margaui.md](./margaui.md) — setting up MargaUI styling: install
|
|
896
|
-
(CDN / npm / vendoring), theme CSS, and `compileClassesToStyleText`.
|
|
897
|
-
- [semantics.md](./semantics.md) — runtime semantics: path steps, the
|
|
898
|
-
transaction lifecycle, dyn-var teleporting, and async key pinning
|
|
899
|
-
(`livePath`).
|
|
900
|
-
- [testing.md](./testing.md) — `getTests` shape and the handler calling
|
|
901
|
-
convention for tests.
|
|
902
|
-
- [storybook.md](./storybook.md) — authoring `*.dev.js` story modules and
|
|
903
|
-
running / embedding the storybook.
|
|
904
|
-
- [cli.md](./cli.md) — commands, flags, exit codes, and the full linter rule
|
|
905
|
-
list.
|
|
906
|
-
- [patterns/README.md](./patterns/README.md) — task-oriented recipes ("how do I
|
|
907
|
-
iterate / filter / paginate / show-hide / build tabs / share state / …"),
|
|
908
|
-
each linking back here and to a runnable example.
|