tutuca 0.9.44 → 0.9.45

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/skill/core.md DELETED
@@ -1,766 +0,0 @@
1
- # Tutuca Cheatsheet — Core
2
-
3
- Tutuca is an immutable-state web framework: components have typed `fields`,
4
- auto-generated mutators (`setX`, `pushInX`, …), HTML-template `view`s with
5
- `@`-prefixed directives, and `bubble` / `receive` / `response` 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
- > Advanced topics (drag & drop, dynamic bindings `*x`, pseudo-`x` for
11
- > `<select>`/`<table>`/`<tr>`, custom seq types, Tailwind/MargaUI
12
- > compilation): see [advanced.md](./advanced.md). CLI commands, flags,
13
- > exit codes, and full linter rule list: see [cli.md](./cli.md). Read
14
- > those only when the task touches them.
15
-
16
- ## Verifying changes
17
-
18
- After editing a Tutuca module, run two checks before declaring the edit
19
- done:
20
-
21
- 1. **Lint the module** — catches undefined fields/handlers/macros/events
22
- (all the `*_NOT_DEFINED` / `*_NOT_REFERENCED` codes):
23
-
24
- tutuca <module-path> lint
25
-
26
- Exits `2` on any error-level finding. Pass a component name to scope
27
- it: `tutuca <module-path> lint Button`.
28
-
29
- 2. **Render the example(s) that exercise the feature you changed** —
30
- confirms the component actually mounts in a headless DOM with the new
31
- behavior. Pick the example whose `title` matches the feature, or
32
- filter by component:
33
-
34
- tutuca <module-path> render --title "Disabled state"
35
- tutuca <module-path> render Button
36
-
37
- Exits `3` if any render crashes. If no example covers the feature
38
- you're adding, add one to `getExamples()` first — that's how the
39
- feature becomes verifiable. Add `--pretty` when you need to read the
40
- emitted HTML to verify structure (attributes, nesting, text); omit it
41
- when you only care that the render didn't crash.
42
-
43
- Full reference: [cli.md](./cli.md).
44
-
45
- ## Common pitfalls
46
-
47
- - **Paths are not allowed in values.** `.foo` resolves a single field or
48
- method on `this` — `@text=".foo.bar"`, `:value=".user.name"`,
49
- `@show=".item.isOpen"` all fail. To reach into nested data: render the
50
- child as a component (`<x render=".foo">` then `@text=".bar"` inside),
51
- add a method (`fullName() { return this.user.name; }` and use
52
- `.fullName`), or use `@enrich-with` for scope-level derivation.
53
- - **Coercion is shallow.** `setItems([{a:1}])` stores plain objects inside
54
- the `List`. Wrap each item in `Comp.make({...})` or run inputs through
55
- immutable's `fromJS` if you need deep coercion. See *Component Skeleton*.
56
- - **Multiple `@if.<attr>` on one element.** Every `@then`/`@else` after
57
- the first must name the attr (`@then.title`, `@else.title`) — HTML
58
- disallows duplicate attrs, so the second `@then=` is dropped silently.
59
- - **Bare unquoted multi-word strings outside `{…}` return `null`.** Either
60
- quote (`'flex gap-3'`) or use template form (`flex gap-3 {.color}`).
61
- - **`<x>` is stripped inside `<select>` / `<table>` / `<tr>`.** Use the
62
- `@x` pseudo-x trick (see [advanced.md](./advanced.md)).
63
- - **`receive.init` is a convention, not a lifecycle hook.** Nothing calls it
64
- automatically — dispatch via `app.sendAtRoot("init")` or from
65
- another handler.
66
- - **`app.state.set(...)` takes a component instance**, not plain data.
67
- Build with `Comp.make({...})`.
68
- - **`html\`` templates must start with the opening tag.** A leading
69
- newline / indent before the first element renders blank silently.
70
- Use `view: html\`<el ...>` (or `html\`<el<newline> attr<newline>>...`),
71
- never `view: html\`<newline> <el ...>`. Same applies to macro bodies.
72
- - **Macro registry keys are normalized to lowercase.** The HTML parser
73
- lowercases custom tag names, so `<x:Card>` is read as `<x:card>`.
74
- `registerMacros({ Card })` is fine — the key is stored as `card`.
75
- Registering two different macros under the same lowercased name (e.g.
76
- `Card` and `card`) warns via `console.assert`; the later one wins.
77
-
78
- ## Bootstrap
79
-
80
- ```js
81
- import { component, html, tutuca } from "tutuca";
82
-
83
- const Counter = component({
84
- name: "Counter",
85
- fields: { count: 0 },
86
- methods: {
87
- inc() {
88
- return this.setCount(this.count + 1);
89
- },
90
- },
91
- view: html`<button @on.click=".inc" @text=".count"></button>`,
92
- });
93
-
94
- const app = tutuca("#app");
95
- app.registerComponents([Counter]);
96
- app.state.set(Counter.make({}));
97
- app.start();
98
- ```
99
-
100
- `app.onChange((info) => ...)` fires after every state change with
101
- `{ val, old, info, timestamp }` (logging, persistence). `app.stop()`
102
- removes all listeners and cancels cache eviction; pair with
103
- `app.start()` for teardown in tests or SPA navigation.
104
-
105
- ## Notation Reference
106
-
107
- | Prefix | Means | Example |
108
- | -------- | ----------------------------------------- | --------------------- |
109
- | `.x` | field or method on `this` (single-level — no `.foo.bar` paths) | `.count`, `.inc` |
110
- | `@x` | local binding (loop / scope) | `@key`, `@value` |
111
- | `^x` | macro parameter | `^label` |
112
- | `!x` | request handler | `!loadData` |
113
- | `*x` | dynamic binding — see `advanced.md` | `*theme` |
114
- | `Name` | component type (PascalCase) | `Item`, `JsonNull` |
115
- | `name` | bare identifier — meaning depends on slot | `dec`, `value`, `ctx` |
116
- | `'str'` | string literal | `'btn btn-success'` |
117
- | `{expr}` | interpolation in attr text | `Hi {.name}` |
118
- | `.s[.k]` | sequence/map item access | `.byKey[.currentKey]` |
119
-
120
- A bare `name` (no prefix) in `@on.<event>="<handler> <arg> <arg>..."`
121
- resolves by slot:
122
-
123
- - **First slot** — handler name looked up in `input` / `alter` (use
124
- `.name` for `methods`).
125
- - **Subsequent slots** — built-in handler arg name (full list in
126
- *Event Handling*); anything else triggers a lint warning.
127
-
128
- ```html
129
- <button @on.click="addItem JsonSelector ctx">+</button>
130
- <!-- ↑ handler ↑ Type ↑ built-in arg -->
131
- ```
132
-
133
- ## Quoting & String Literals
134
-
135
- Tutuca's expression parser is context-sensitive. `:attr=` and
136
- interpolation `{...}` accept string templates; `@if`, `@each`,
137
- `<x render=>` do not.
138
-
139
- | Form | Example | Where it works |
140
- | ------------------- | ---------------------- | ------------------------------------------------ |
141
- | `'string'` | `@then="'btn ok'"` | anywhere a value is allowed |
142
- | Bare with `{...}` | `:class="btn {.kind}"` | `:attr=`, `@text`, `@title`, macro dynamic attrs |
143
- | Bare without quotes | `flex gap-3` | **never** — returns `null` |
144
- | Bare identifier | `dec`, `value` | name slots only (handler/arg, not as a value) |
145
-
146
- ```html
147
- <!-- ✅ -->
148
- <p :class="'flex gap-3'">x</p>
149
- <p :class="flex {.color}">x</p> <!-- {…} enables template -->
150
- <p :class="static-classes {''}">x</p> <!-- folds to a const -->
151
-
152
- <!-- ❌ -->
153
- <p :class="flex gap-3">x</p> <!-- null: no quotes, no braces -->
154
- <x render="'foo bar'"></x> <!-- @render rejects string templates -->
155
- ```
156
-
157
- ## Component Skeleton
158
-
159
- ```js
160
- component({
161
- name: "MyComp",
162
- fields: { // see "Field Types"
163
- count: 0,
164
- items: [],
165
- nullable: null,
166
- },
167
- view: html`<p @text=".count"></p>`, // default view (named "main")
168
- views: { // additional views
169
- edit: html`<input :value=".count" @on.input=".setCount valueAsInt" />`,
170
- big: {
171
- view: html`<h1 @text=".count"></h1>`,
172
- style: css`h1 { font-size: 4rem; }`,
173
- },
174
- },
175
- style: css`p { color: blue; }`, // scoped to main view
176
- commonStyle: css`p { font-family: sans-serif; }`, // scoped to all views of this component
177
- globalStyle: css`body { margin: 0; }`, // injected globally, no scoping
178
- methods: { inc() { return this.setCount(this.count + 1); } },
179
- input: { onClick(ctx) { return this.inc(); } },
180
- alter: { filterItem(_k, item) { return item.length > 0; } },
181
- receive: { init(ctx) { ctx.request("loadData"); return this; } },
182
- bubble: { itemPicked(item, ctx) { return this.setSelected(item); } },
183
- response:{ loadData(res, err, ctx) { return this.setItems(res); } },
184
- statics: { fromData(d) { return this.make({ count: d.n ?? 0 }); } },
185
- // dynamic: { ... }, on: { stackEnter() {...} } // see advanced.md
186
- });
187
- ```
188
-
189
- `Comp.make({...})` builds an instance. Coercion is automatic but
190
- **shallow**: arrays become `List`, plain objects become `IMap`, native
191
- `Set` becomes `ISet`. Items inside a list/map field stay as-is —
192
- `setItems([{a:1}])` gives `List<plainObject>`; access with `item.a`, not
193
- `item.get("a")`. For deep coercion, run inputs through immutable's
194
- `fromJS`, or wrap each item in `Comp.make({...})`.
195
-
196
- ## Field Types & Auto-generated API
197
-
198
- `fields: { name: defaultValue }` — type inferred from the default.
199
-
200
- | Default | Field type | Auto-generated methods (for field `x`) |
201
- | -------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
202
- | `"hi"` | text | `setX`, `updateX`, `resetX`, `isXTruthy`, `isXFalsy`, `isXEmpty`, `xLen` |
203
- | `42` | float | `setX`, `updateX`, `resetX`, `isXTruthy`, `isXFalsy` |
204
- | (`{type:"int"}`) | int | `setX`, `updateX`, `resetX`, `isXTruthy`, `isXFalsy` (no default-value form — declare explicitly via `classFromData`) |
205
- | `true` | bool | `setX`, `toggleX`, `updateX`, `resetX`, `isXTruthy`, `isXFalsy` |
206
- | `null` | any | `setX`, `updateX`, `resetX`, `isXNull`, `isXTruthy`, `isXFalsy` |
207
- | `[]`/`List()` | list | `setX`, `pushInX`, `insertInXAt`, `setInXAt`, `getInXAt`, `updateInXAt`, `deleteInXAt`/`removeInXAt`, `isXEmpty`, `isXTruthy`, `isXFalsy`, `xLen`, `resetX` |
208
- | `{}`/`IMap()` | map | `setInXAt`, `getInXAt`, `updateInXAt`, `deleteInXAt`, `isXEmpty`, `isXTruthy`, `isXFalsy`, `xLen`, `resetX` |
209
- | `OMap()` | omap | same as map (preserves insertion order) |
210
- | `ISet()`/`new Set()` | set | `addInX`, `deleteInX`, `hasInX`, `toggleInX`, `xLen`, `isXEmpty`, `isXTruthy`, `isXFalsy`, `resetX` |
211
-
212
- Explicit field types via `classFromData`:
213
-
214
- ```js
215
- fields: {
216
- count: { type: "int", defaultValue: 10 }, // text/int/float/bool/list/map/omap/set/any
217
- child: { component: "Item", args: { ... } }, // nested component instance
218
- }
219
- ```
220
-
221
- ## Methods as Predicates & Computed Values
222
-
223
- A method called via field syntax (`.name`, no args) is invoked and its
224
- return value is used. Works anywhere a value is read — `@text`, `:attr`,
225
- `@show` / `@hide`, `@if.<attr>`, and `{…}` interpolation.
226
-
227
- ```js
228
- methods: {
229
- canSubmit() { return this.title.length > 0 && !this.isLoading; },
230
- buttonClass() { return this.isActive ? "btn btn-primary" : "btn"; },
231
- fullName() { return `${this.first} ${this.last}`; },
232
- }
233
- ```
234
-
235
- ```html
236
- <button @show=".canSubmit" :class=".buttonClass">Save</button>
237
- <p :title="Hello, {.fullName}" @text=".fullName"></p>
238
- ```
239
-
240
- Auto-generated `isXTruthy` / `isXEmpty` / `isXNull` cover single-field
241
- checks; reach for a method when the predicate spans multiple fields or
242
- needs derivation. The method takes no args.
243
-
244
- Tutuca expressions resolve a **single** name on `this` — there is no
245
- path syntax. `@text=".user.name"` does not navigate; it fails. When the
246
- value lives behind a field, your options are:
247
-
248
- - **Render the child as a component** — `<x render=".user">` then
249
- `@text=".name"` inside the child's view. Best when the nested thing is
250
- already (or could be) a component.
251
- - **Add a method** — `userName() { return this.user.name; }` then
252
- `@text=".userName"`. Best for one-off derivations or formatting.
253
- - **Use `@enrich-with`** — exposes computed values as `@`-bindings to a
254
- subtree without putting them on the component. See *Scope Enrichment*.
255
-
256
- Exceptions: `@each` / `render-each` accept `.field` or `*dynamic` only
257
- (not a method call), and `<x render>` expects a component instance — for
258
- a derived list, store it in a field or use `@when` with `alter`.
259
-
260
- ## Statics
261
-
262
- `statics: {...}` adds methods to the component **class**, not instances.
263
- Available as `Comp.Class.<name>(...)` alongside the auto-generated
264
- `Comp.Class.make(...)` (which `Comp.make(...)` aliases). Inside a static,
265
- `this` is the class itself.
266
-
267
- Common use: a `fromData` factory that recursively builds instances from
268
- plain JS data:
269
-
270
- ```js
271
- statics: {
272
- fromData({ items = [] }) {
273
- return this.make({ items: items.map((v) => Item.Class.fromData(v)) });
274
- },
275
- }
276
- // usage: TreeRoot.Class.fromData([...])
277
- ```
278
-
279
- ## Text Rendering
280
-
281
- ```html
282
- <span @text=".str"></span> <!-- prepend text into span -->
283
- <x text=".bool"></x> <!-- text-only, no DOM element -->
284
- <x text=".getStrUpper"></x> <!-- methods are called -->
285
- <x text="@value"></x> <!-- loop binding -->
286
- ```
287
-
288
- ## Attribute Binding
289
-
290
- ```html
291
- <input :value=".str" @on.input=".setStr value" />
292
- <a :href=".url" :title="Hi {.name}">link</a> <!-- string template -->
293
- <button class="btn" :class="btn {.color}">x</button>
294
- ```
295
-
296
- Plain attrs are static. `:attr="..."` is a dynamic expression. Boolean
297
- HTML attributes (`disabled`, `checked`, `hidden`, …) are auto-recognized;
298
- pass a boolean field.
299
-
300
- The HTML parser lowercases attribute names before Tutuca sees them, so
301
- `:mapId` arrives as `:mapid` and `<x:Card>` becomes `<x:card>`. Three
302
- consequences:
303
-
304
- - SVG attributes are case-sensitive. Tutuca special-cases `:viewbox` →
305
- `viewBox` so SVG roots work; for other camelCased SVG attrs, wrap them
306
- in components that emit raw markup.
307
- - Custom-element property setters defined in camelCase **will not fire**.
308
- `:mapId=".mapId"` runs `node.mapid = value`; if the
309
- element defined `set mapId(...)`, the lookup misses and JS silently
310
- creates an own data property `mapid` on the element instead of invoking
311
- the setter — no error, no warning, the bound state stays null. Author
312
- custom elements with kebab-case attributes plus lowercased property
313
- setters (or aliases), and bind via `:kebab-name` from Tutuca templates.
314
- - Macro registry keys are lowercased on insert for the same reason
315
- (see "Macros" section below).
316
-
317
- ## Event Handling
318
-
319
- ```html
320
- <!-- method (`.`) vs input handler (no dot) -->
321
- <button @on.click=".inc">+</button>
322
- <button @on.click="dec">-</button>
323
-
324
- <!-- pass args by name -->
325
- <input @on.input=".setStr value" />
326
- <input @on.input=".setN valueAsInt" />
327
- <button @on.click=".pick @key isAlt">pick</button>
328
- <button @on.click=".addItem JsonSelector">+</button> <!-- type as arg -->
329
- <button @on.click=".loadAnotherWay ctx">load</button> <!-- ctx -->
330
- ```
331
-
332
- Built-in handler arg names: `value`, `valueAsInt`, `valueAsFloat`,
333
- `target`, `event`, `isAlt`, `isShift`, `isCtrl`/`isCmd`, `key`, `keyCode`,
334
- `isUpKey`, `isDownKey`, `isSend`, `isCancel`, `isTabKey`, `ctx`,
335
- `dragInfo`.
336
-
337
- The content of `value` depends on the event source:
338
-
339
- | Source | What `value` resolves to |
340
- |-----------------------------|--------------------------------------------------|
341
- | `<input type="checkbox">` | `event.target.checked` (boolean) |
342
- | `CustomEvent` | `event.detail` |
343
- | anything else | `event.target.value` (string), or null if absent |
344
-
345
- For numeric inputs, prefer `valueAsInt` / `valueAsFloat` to skip the
346
- string parse.
347
-
348
- ### Event Modifiers
349
-
350
- `@on.<event>+<mod>+<mod>=...`
351
-
352
- - All events: `+ctrl`, `+cmd`/`+meta`, `+alt`
353
- - `keydown` only: `+send` (Enter), `+cancel` (Escape)
354
-
355
- ```html
356
- <input @on.keydown+send=".submit value" @on.keydown+cancel=".reset" />
357
- <button @on.click+ctrl=".soloOnly">ctrl-click</button>
358
- ```
359
-
360
- ### Web Components & Custom Events
361
-
362
- Custom elements just work, and any `CustomEvent` they fire is reachable
363
- via `@on.<event-name>`. The event's `detail` surfaces as `value`:
364
-
365
- ```js
366
- import "https://esm.sh/emoji-picker-element";
367
-
368
- input: { onPick(detail) { return this.setCurrent(detail.unicode); } }
369
- view: html`<emoji-picker @on.emoji-click="onPick value"></emoji-picker>`,
370
- ```
371
-
372
- Pitfall: binding camelCase JS properties on a custom element silently
373
- fails. `:mapId=".id"` does *not* invoke a `set mapId` setter
374
- — the HTML parser lowercased the attribute name, so the framework assigns
375
- to `node.mapid` instead, creating an own property and bypassing the
376
- setter. Use kebab-case attributes / lowercased setters when authoring
377
- custom elements for use with Tutuca. See "Attribute Binding" above.
378
-
379
- ## Conditional Display
380
-
381
- ```html
382
- <div @show=".isLoading">Loading...</div>
383
- <div @hide=".isLoading">content</div>
384
-
385
- <!-- show / hide also work as wrapper attrs on `<x>` render ops:
386
- wraps the produced node, no extra DOM element. Allowed on
387
- text / render / render-it / render-each. First attr in
388
- source order becomes the outermost wrapper. -->
389
- <x text=".name" show=".isOpen"></x>
390
- <x render-it hide=".isHidden"></x>
391
- <x render-each=".items" when="filter" show=".isOpen"></x>
392
-
393
- <!-- Single @if: shorthand @then/@else (attr inferred) -->
394
- <button @if.class=".isActive" @then="'btn btn-success'" @else="'btn btn-ghost'">
395
- ...
396
- </button>
397
-
398
- <!-- Multiple @if on same element: name the attr explicitly -->
399
- <button
400
- @if.class=".isActive"
401
- @then="'on'"
402
- @else="'off'"
403
- @if.title=".isActive"
404
- @then.title="'On'"
405
- @else.title="'Off'"
406
- >
407
- ...
408
- </button>
409
- ```
410
-
411
- > HTML disallows duplicate attrs, so with multiple `@if.<attr>` on one
412
- > element every `@then`/`@else` after the first **must** include the attr
413
- > name — otherwise the parser drops it before tutuca sees it.
414
-
415
- ## List Iteration
416
-
417
- `@each` accepts: `.field`, `*dynamic`.
418
-
419
- ```html
420
- <!-- iterate plain values -->
421
- <li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
422
-
423
- <!-- filter -->
424
- <li @each=".items" @when="filterItem">...</li>
425
-
426
- <!-- per-item enrichment (binds.X => @X in template) -->
427
- <li @each=".items" @enrich-with="enrichItem">
428
- <x text="@count"></x>
429
- </li>
430
-
431
- <!-- shared per-loop data (computed once before iteration) -->
432
- <li @each=".items" @loop-with="getIterData" @when="filterItem">...</li>
433
-
434
- <!-- render a list of components -->
435
- <x render-each=".items"></x>
436
- <x render-each=".items" as="edit"></x> <!-- specific view -->
437
- <x render-each=".items" when="filterItem"></x> <!-- with filter -->
438
- <x render-each=".items" loop-with="getIterData" when="filterItem"></x>
439
- <x render-each=".items" show=".isOpen"></x> <!-- wrap in show -->
440
- ```
441
-
442
- On `<li @each>` / `<div @each>` and other host-element loops the
443
- filters are written `@when` / `@enrich-with` / `@loop-with` (the `@`
444
- prefix is the element-directive convention). On `<x render-each>` the
445
- same filters drop the prefix — `when=` / `enrich-with=` / `loop-with=`
446
- — because `<x>` carries plain attributes, not directives. Both forms
447
- share the handler-name resolution rules below.
448
-
449
- ```js
450
- alter: {
451
- filterItem(_key, item, iterData) { return item.includes(iterData.q); },
452
- enrichItem(binds, _key, item, iterData) { binds.count = item.length; },
453
- getIterData(seq) { return { q: this.query.toLowerCase() }; },
454
- }
455
- ```
456
-
457
- ### Lifecycle of `@each`
458
-
459
- For each render of an element with `@each=".items"`:
460
-
461
- 1. **Resolve sequence** — evaluate `.items`. Lists, IMaps, OMaps, ISets,
462
- and any class declaring a `SEQ_INFO` walker are recognized.
463
- 2. **`@loop-with`** (once per render) — `getIterData.call(this, seq)` is
464
- called and its return becomes `iterData`. Skipped if no `@loop-with`;
465
- in that case `iterData` is `{ seq }` from the default.
466
- 3. For each `(key, value)` pair in the sequence:
467
- 1. **`@when`** — `filterItem.call(this, key, value, iterData)`; if it
468
- returns `false`, the item is skipped.
469
- 2. **`@enrich-with`** — `enrichItem.call(this, binds, key, value, iterData)`.
470
- `binds` is a **mutable object** seeded with `{ key, value }`;
471
- mutating it (`binds.count = ...`) creates `@`-prefixed bindings
472
- available in the templated children. The return value is ignored.
473
- 3. **Render** the element with the new bindings on the stack.
474
-
475
- Auto-bound names inside the loop are always `@key` and `@value` (or
476
- whatever you wrote into `binds`).
477
-
478
- ### Handler resolution
479
-
480
- `@when` / `@enrich-with` / `@loop-with` resolve like event handler names:
481
- bare `filterItem` → `alter.filterItem` (idiomatic); `.filterItem` →
482
- method on `this` (works, not idiomatic — `alter` keeps iteration helpers
483
- grouped).
484
-
485
- ## Scope Enrichment
486
-
487
- Without an `@each` on the same element, `@enrich-with` becomes a scope
488
- enricher: it takes no `binds` arg, and its **return value** is the
489
- bindings object whose keys become `@`-prefixed bindings for descendants.
490
-
491
- ```js
492
- alter: { enrichScope() { return { len: this.text.length }; } }
493
- ```
494
-
495
- ```html
496
- <div @enrich-with="enrichScope">Length: <x text="@len"></x></div>
497
- ```
498
-
499
- ## Rendering Components
500
-
501
- ```html
502
- <x render=".item"></x> <!-- default ("main") view -->
503
- <x render=".item" as="edit"></x> <!-- specific view -->
504
- <x render-it></x> <!-- only inside @each / render-each -->
505
- <x render=".byIndex[.currentIndex]"></x> <!-- list item access -->
506
- <x render=".byKey[.currentKey]"></x> <!-- map item access -->
507
- <x render=".item" show=".isOpen"></x> <!-- conditional wrap, see "Conditional Display" -->
508
- ```
509
-
510
- The top-level `view` is registered under `"main"` (the default); extras
511
- go under `views: { name: html\`...\` }`. `as="edit"` selects the `edit`
512
- view of the rendered component, falling back to `main` if absent. `as`
513
- only applies to the **direct** component — for whole-subtree control,
514
- use `@push-view` (next section).
515
-
516
- ## Multiple Views & View Stack
517
-
518
- ```js
519
- component({
520
- view: html`<p @text=".title"></p>`, // "main"
521
- views: { edit: html`<input :value=".title" @on.input=".setTitle value" />` },
522
- });
523
- ```
524
-
525
- ```html
526
- <!-- @push-view pushes a name onto the rendering stack;
527
- descendants resolve to first matching view, falling back to "main" -->
528
- <div @push-view=".view"><x render-each=".items"></x></div>
529
- ```
530
-
531
- | Directive | Scope |
532
- |--------------------|--------------------------------------------------------------------------|
533
- | `as="edit"` | One `<x render>` element only. |
534
- | `@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. |
535
-
536
- ## Styles
537
-
538
- ```js
539
- component({
540
- style: css`.mine { color: red; }`, // scoped to main view
541
- commonStyle: css`.shared { color: yellow; }`, // scoped to all views of this component
542
- globalStyle: css`.app-thing { color: green; }`, // global, no scoping
543
- views: {
544
- two: { view: html`...`, style: css`.mine { color: orange; }` },
545
- },
546
- });
547
- ```
548
-
549
- Tagged templates `html` and `css` are just `String.raw` (editor hinting
550
- only). Plain strings work too.
551
-
552
- ## Triggers and Handlers
553
-
554
- Tutuca has four orchestration channels. Each one pairs a trigger with
555
- a same-shape handler block:
556
-
557
- | Triggered by | Handler block |
558
- | ------------------------------------------- | ------------------- |
559
- | DOM event (`click`, `input`, …) | `input: { ... }` |
560
- | `ctx.send(name)` — message to a target path | `receive: { ... }` |
561
- | `ctx.request(name)` — async request | `response: { ... }` |
562
- | `ctx.bubble(name)` — event up the tree | `bubble: { ... }` |
563
-
564
- Every handler is called as `handler(...args, ctx)` and returns a
565
- (possibly updated) instance of `this`; the framework swaps the
566
- returned value into the dispatch path. The four sections below cover
567
- each channel in turn.
568
-
569
- ## Bubble Events
570
-
571
- ```js
572
- input: { onClick(ctx) { ctx.bubble("treeItemSelected", [this]); return this; } },
573
- bubble: {
574
- treeItemSelected(selected, ctx) { // ctx.stopPropagation() to halt
575
- return this.insertInLogAt(0, `selected ${selected.label}`);
576
- },
577
- }
578
- ```
579
-
580
- `ctx.bubble("name", args)` emits an event that walks the dispatch path
581
- back toward the root. Each ancestor whose component defines
582
- `bubble.<name>(...args, ctx)` runs it (others are skipped silently);
583
- bubbling stops at the root or when a handler calls
584
- `ctx.stopPropagation()`. Ancestors see the event *after* descendants
585
- have transacted, so bubble handlers are the place for aggregate state
586
- (logs, selections, totals).
587
-
588
- ## Send / Receive
589
-
590
- `ctx.send(name, args)` delivers a message to a specific target
591
- component (addressed by path; on its own `ctx.send` targets `this`).
592
- The target's `receive.<name>(...args, ctx)` handler runs. There is
593
- **no built-in lifecycle** — `receive.init` is just a convention; the
594
- host must dispatch it (typically after `app.start()`) for it to run.
595
-
596
- ```js
597
- receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } }
598
- ```
599
-
600
- Dispatch from anywhere:
601
-
602
- ```js
603
- app.sendAtRoot("init"); // host code, top-level
604
- ctx.at.field("personalSite").send("init"); // child by field name
605
- ctx.at.index("items", 3).send("init"); // list element at index 3
606
- ctx.at.key("byKey", "k1").send("init"); // map entry by key
607
- ctx.at.field("a").field("b").index("xs", 0).send("ping"); // chain freely
608
- ctx.send("name"); // self
609
- ctx.bubble("name", [arg]); // bubble up
610
- ```
611
-
612
- `ctx.at` returns a `PathBuilder` with `.field(name)`, `.index(name, i)`,
613
- and `.key(name, k)`. Each call appends a step to the path before
614
- `.send(...)` / `.bubble(...)` fires; the handler runs inside the child
615
- instance with `this` bound to it.
616
-
617
- ## Async Requests
618
-
619
- `ctx.request("name", args)` triggers a host-registered async handler
620
- and routes the result back to the issuing component's
621
- `response.<name>(res, err, ctx)`. Use it for fetch / timer / IndexedDB
622
- work that should land back in component state.
623
-
624
- ```js
625
- export function getRequestHandlers() {
626
- return {
627
- async loadData() {
628
- const r = await fetch("https://example.com/data.json");
629
- return await r.json();
630
- },
631
- };
632
- }
633
-
634
- // register at the same scope where you registerComponents
635
- const scope = app.registerComponents([Comp]);
636
- scope.registerRequestHandlers(getRequestHandlers());
637
- ```
638
-
639
- In a component:
640
-
641
- ```js
642
- receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
643
- response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
644
- // override response handler names per-call:
645
- // ctx.request("loadData", [], { onOkName: "loadDataOk", onErrorName: "loadDataErr" });
646
- ```
647
-
648
- The `ctx` arg is the last argument of every `response` / `bubble` /
649
- `receive` handler.
650
-
651
- ## Macros
652
-
653
- Pure template expansion — no state, no methods. Calls inside a macro
654
- resolve against the *host* component.
655
-
656
- ```js
657
- import { macro, html } from "tutuca";
658
-
659
- const badge = macro(
660
- { label: "'New'", kind: "'info'" }, // defaults are *expressions*
661
- html`<span :class="badge badge-{^kind}" @text="^label"></span>`,
662
- );
663
-
664
- export function getMacros() {
665
- return { badge };
666
- }
667
- ```
668
-
669
- ```html
670
- <x:badge></x:badge> <!-- defaults -->
671
- <x:badge label="Sale"></x:badge> <!-- static string (no quotes needed) -->
672
- <x:badge :label="'Sale'"></x:badge> <!-- dynamic literal -->
673
- <x:badge :label=".status"></x:badge> <!-- field reference -->
674
- ```
675
-
676
- Register macros at the same scope as components:
677
-
678
- ```js
679
- const scope = app.registerComponents([Comp]);
680
- scope.registerMacros(getMacros());
681
- ```
682
-
683
- Registry keys are lowercased on insert because the HTML parser already
684
- lowercases `<x:Tag>` to `<x:tag>`. `{ Card }` and `{ card }` both register
685
- under `card`; registering two *different* macros under the same lowercased
686
- name warns via `console.assert`.
687
-
688
- ### Slots
689
-
690
- ```js
691
- const card = macro(
692
- { title: "'Card'" },
693
- html`<div class="card">
694
- <h2 @text="^title"></h2>
695
- <x:slot></x:slot>
696
- </div>`,
697
- );
698
- ```
699
-
700
- ```html
701
- <x:card title="Hi"><p>body</p></x:card> <!-- default slot -->
702
- ```
703
-
704
- ### Named Slots
705
-
706
- ```js
707
- const panel = macro(
708
- {},
709
- html`<div>
710
- <header><x:slot name="actions"></x:slot></header>
711
- <main><x:slot></x:slot></main> <!-- default == name="_" -->
712
- <footer><x:slot name="footer"></x:slot></footer>
713
- </div>`,
714
- );
715
- ```
716
-
717
- ```html
718
- <x:panel>
719
- <x slot="actions"><button @on.click=".inc">+</button></x>
720
- <p>default slot content</p>
721
- <x slot="footer">© 2026</x>
722
- </x:panel>
723
- ```
724
-
725
- ## Raw HTML (escape hatch)
726
-
727
- ```html
728
- <div @dangerouslysetinnerhtml=".trustedHtml"></div>
729
- ```
730
-
731
- Bypasses all escaping; children of the element are ignored when active.
732
-
733
- ## Immutable Re-exports
734
-
735
- `tutuca` re-exports **everything** from
736
- [`immutable`](https://immutable-js.com/) (`List`, `OrderedMap`, `Record`,
737
- `Seq`, `is`, `fromJS`, ...), plus short aliases to avoid clashes with
738
- the host runtime's `Map` / `Set`:
739
-
740
- ```js
741
- import {
742
- IMap, OMap, ISet, // aliases for Map, OrderedMap, Set
743
- isIMap, isOMap, // aliases for isMap, isOrderedMap
744
- List, Record, Seq, fromJS, is, // ...everything else immutable exports
745
- } from "tutuca";
746
- ```
747
-
748
- ## Conventional Module Exports
749
-
750
- Examples and the storybook glue follow this shape so files compose freely
751
- and the `tutuca` CLI can introspect any module without per-app glue:
752
-
753
- ```js
754
- export function getComponents() { return [Comp, ...]; }
755
- export function getMacros() { return { name: macro }; } // optional
756
- export function getRequestHandlers() { return { name: async fn }; } // optional
757
- export function getRoot() { return Root.make({...}); }
758
- export function getExamples() {
759
- // Return one section, or an array of sections.
760
- return {
761
- title: "...",
762
- description: "...",
763
- items: [{ title, description, value, view }], // value = Comp.make(...)
764
- };
765
- }
766
- ```