tutuca 0.9.79 → 0.9.80
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/README.md +1 -1
- package/package.json +1 -1
- package/skill/tutuca/SKILL.md +4 -3
- package/skill/tutuca/core.md +6 -1
- package/skill/tutuca/patterns/README.md +42 -0
- package/skill/tutuca/patterns/bind-text-and-attributes.md +30 -0
- package/skill/tutuca/patterns/conditional-attribute-value.md +29 -0
- package/skill/tutuca/patterns/coordinate-components.md +26 -0
- package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +27 -0
- package/skill/tutuca/patterns/enrich-each-item.md +25 -0
- package/skill/tutuca/patterns/filter-a-list.md +23 -0
- package/skill/tutuca/patterns/handle-events.md +27 -0
- package/skill/tutuca/patterns/iterate-a-list.md +18 -0
- package/skill/tutuca/patterns/paginate-a-list.md +27 -0
- package/skill/tutuca/patterns/render-a-child-component.md +20 -0
- package/skill/tutuca/patterns/reuse-markup-with-macros.md +35 -0
- package/skill/tutuca/patterns/share-state-without-prop-drilling.md +30 -0
- package/skill/tutuca/patterns/show-or-hide-content.md +22 -0
- package/skill/tutuca/patterns/switch-between-views.md +26 -0
- package/skill/tutuca/patterns/tabbed-interface.md +38 -0
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ npx tutuca install-skill --dot-agents
|
|
|
168
168
|
npx tutuca install-skill --force
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
The skill content is generated from `docs/
|
|
171
|
+
The skill content is generated from `docs/skill/`, so the same reference
|
|
172
172
|
runs locally (`tutuca lint <module>` + `tutuca render <module> --title …`)
|
|
173
173
|
and inside Claude.
|
|
174
174
|
|
package/package.json
CHANGED
package/skill/tutuca/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tutuca
|
|
3
|
-
description: Use when authoring or reviewing tutuca modules — `component({...})` definitions, `html
|
|
3
|
+
description: Use when authoring or reviewing tutuca modules — `component({...})` definitions, `html`...`` views, `@`-directives, `input` / `bubble` / `receive` / `response` / `alter` handlers, macros, or `getTests` exports — or when running the `tutuca` CLI (`lint` / `test` / `render` / `docs`). Covers the post-edit `tutuca <module> lint` → `test` → `render --title "<example>"` verification recipe.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!--
|
|
6
|
+
<!-- Source of truth: docs/skill/. The skill/tutuca/ copy is generated by scripts/build-skill.js — edit docs/skill/, not skill/. -->
|
|
7
7
|
|
|
8
8
|
# Tutuca
|
|
9
9
|
|
|
@@ -27,12 +27,13 @@ When authoring tutuca code, also load these if available:
|
|
|
27
27
|
|
|
28
28
|
| Task | File |
|
|
29
29
|
| ---------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
30
|
-
| Authoring `component({...})`, `html
|
|
30
|
+
| Authoring `component({...})`, `html`...`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
|
|
31
31
|
| CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
|
|
32
32
|
| `bubble` / `send`-`receive` / async `request`-`response` channels, `$unknown`, request-handler registration | [request-response.md](./request-response.md) |
|
|
33
33
|
| Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
|
|
34
34
|
| Runtime semantics — path steps, transaction lifecycle, dyn-var teleporting, async key pinning (`livePath`) | [semantics.md](./semantics.md) |
|
|
35
35
|
| Authoring tests — `getTests` shape, calling methods/input/receive/bubble/response/alter handlers, designing handlers for testability | [testing.md](./testing.md) |
|
|
36
|
+
| Task-oriented recipes — iteration, filtering, conditional content, conditional attributes, dynamic vars, composition, events | [patterns/README.md](./patterns/README.md) |
|
|
36
37
|
|
|
37
38
|
Read `core.md` first. Reach for the others only when the task touches
|
|
38
39
|
them — each is referenced inline from `core.md` so you'll be pointed
|
package/skill/tutuca/core.md
CHANGED
|
@@ -19,7 +19,9 @@ the `tutuca` CLI.
|
|
|
19
19
|
> [testing.md](./testing.md). Runtime semantics — path steps, the
|
|
20
20
|
> transaction lifecycle, dyn-var teleporting, and async key pinning
|
|
21
21
|
> (`livePath`): see [semantics.md](./semantics.md). Read those only when
|
|
22
|
-
> the task touches them.
|
|
22
|
+
> the task touches them. Task-oriented "how do I do X" recipes (iteration,
|
|
23
|
+
> filtering, slicing, conditional content, conditional attributes, dynamic
|
|
24
|
+
> vars, composition, events, …): see [patterns/README.md](./patterns/README.md).
|
|
23
25
|
|
|
24
26
|
## Verifying changes
|
|
25
27
|
|
|
@@ -987,3 +989,6 @@ export function getTests({ describe, test, expect }) { /*...*/ } // optiona
|
|
|
987
989
|
convention for tests.
|
|
988
990
|
- [cli.md](./cli.md) — commands, flags, exit codes, and the full linter rule
|
|
989
991
|
list.
|
|
992
|
+
- [patterns/README.md](./patterns/README.md) — task-oriented recipes ("how do I
|
|
993
|
+
iterate / filter / paginate / show-hide / build tabs / share state / …"),
|
|
994
|
+
each linking back here and to a runnable example.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Tutuca Patterns
|
|
2
|
+
|
|
3
|
+
Task-oriented recipes: "how do I do X" with a minimal working snippet and the
|
|
4
|
+
one pitfall worth knowing. Each recipe is self-contained and brief; for the
|
|
5
|
+
full directive *semantics* behind a pattern, see [core.md](../core.md) and its
|
|
6
|
+
spokes.
|
|
7
|
+
|
|
8
|
+
New to Tutuca? Read [core.md](../core.md) first, then reach here for a specific
|
|
9
|
+
task.
|
|
10
|
+
|
|
11
|
+
## Iteration & lists
|
|
12
|
+
|
|
13
|
+
- [Iterate a list](iterate-a-list.md) — render one element per item with `@each` / `render-each`.
|
|
14
|
+
- [Filter a list](filter-a-list.md) — keep only matching items with `@when`.
|
|
15
|
+
- [Enrich each item](enrich-each-item.md) — expose derived per-item values as `@`-bindings.
|
|
16
|
+
- [Paginate a list](paginate-a-list.md) — slice the iteration with `@loop-with` `start`/`end`.
|
|
17
|
+
|
|
18
|
+
## Conditional content & attributes
|
|
19
|
+
|
|
20
|
+
- [Show or hide content](show-or-hide-content.md) — `@show` / `@hide` and the boolean predicates.
|
|
21
|
+
- [Switch between views](switch-between-views.md) — pick a component's own view with `as=` or `@push-view`.
|
|
22
|
+
- [Conditional attribute value](conditional-attribute-value.md) — set a class/title by condition with `@if` / `@then` / `@else`.
|
|
23
|
+
- [Tabbed interface](tabbed-interface.md) — a `currentView` field + predicates to show the panel and highlight the active tab.
|
|
24
|
+
|
|
25
|
+
## Context & dynamic variables
|
|
26
|
+
|
|
27
|
+
- [Share state without prop-drilling](share-state-without-prop-drilling.md) — `provide` / `lookup` and reading `*name`.
|
|
28
|
+
- [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and teleport edits back to the owner.
|
|
29
|
+
|
|
30
|
+
## Composition
|
|
31
|
+
|
|
32
|
+
- [Render a child component](render-a-child-component.md) — `<x render=".field">` and multiple views.
|
|
33
|
+
- [Reuse markup with macros](reuse-markup-with-macros.md) — `macro(...)` with parameters and slots.
|
|
34
|
+
|
|
35
|
+
## Data & events
|
|
36
|
+
|
|
37
|
+
- [Bind text and attributes](bind-text-and-attributes.md) — `@text`, `:attr`, `$'…'` templates, scope enrichment.
|
|
38
|
+
- [Handle events](handle-events.md) — `@on.<event>`, handler args, modifiers, custom events.
|
|
39
|
+
|
|
40
|
+
## Component communication
|
|
41
|
+
|
|
42
|
+
- [Coordinate components](coordinate-components.md) — `bubble`, `send`/`receive`, async `request`/`response`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Bind text and attributes
|
|
2
|
+
|
|
3
|
+
**Problem:** display a field as text, bind it to an attribute, or compose a
|
|
4
|
+
string from several values.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<!-- text -->
|
|
8
|
+
<span @text=".str"></span> <!-- into a host element -->
|
|
9
|
+
<x text="$getStrUpper"></x> <!-- $ calls a method; no wrapping element -->
|
|
10
|
+
|
|
11
|
+
<!-- attributes: plain = static, :attr = dynamic -->
|
|
12
|
+
<input :value=".str" @on.input="$setStr value" />
|
|
13
|
+
<a :href=".url" :title="$'Hi {.name}'">link</a> <!-- $'…' string template -->
|
|
14
|
+
<button :class="$'btn btn-{.kind}'">x</button>
|
|
15
|
+
|
|
16
|
+
<!-- derive values for a subtree without putting them on the component -->
|
|
17
|
+
<div @enrich-with="enrichScope">Len: <x text="@len"></x></div>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
methods: { getStrUpper() { return this.str.toUpperCase(); } },
|
|
22
|
+
alter: { enrichScope() { return { len: this.text.length }; } }, // keys → @len, …
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Value slots take `.field`, `$method`, or `@binding` — never a path
|
|
26
|
+
(`.user.name` fails). Multi-word strings **must** be quoted (`'flex gap-3'`) or
|
|
27
|
+
written as a `$'…'` template (`$'btn {.kind}'`); a bare unquoted string returns
|
|
28
|
+
`null`. Boolean HTML attributes (`disabled`, `checked`, …) are auto-recognized
|
|
29
|
+
— pass a boolean field. Scope `@enrich-with` (no `@each` on the element) is the
|
|
30
|
+
path-free way to expose derived values to a subtree.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Conditional attribute value
|
|
2
|
+
|
|
3
|
+
**Problem:** set an attribute (class, title, …) to one value or another
|
|
4
|
+
depending on a condition.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<button
|
|
8
|
+
@if.class=".isActive"
|
|
9
|
+
@then="'btn btn-success'"
|
|
10
|
+
@else="'btn btn-ghost'"
|
|
11
|
+
@on.click="$toggleIsActive"
|
|
12
|
+
>
|
|
13
|
+
toggle
|
|
14
|
+
</button>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`@if.<attr>` takes the condition (a `.field`, a `$method`, or a predicate like
|
|
18
|
+
`equals? .tab 'x'`); `@then`/`@else` are the two values. String literals need
|
|
19
|
+
quotes (`'btn ok'`); a `$'…'` template works too. **Multiple `@if` on one
|
|
20
|
+
element:** every `@then`/`@else` after the first must name its attr
|
|
21
|
+
(`@then.title`, `@else.title`) — HTML forbids duplicate attribute names, so an
|
|
22
|
+
unnamed second `@then` is dropped silently.
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<button
|
|
26
|
+
@if.class=".isActive" @then="'on'" @else="'off'"
|
|
27
|
+
@if.title=".isActive" @then.title="'On'" @else.title="'Off'"
|
|
28
|
+
></button>
|
|
29
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Coordinate components
|
|
2
|
+
|
|
3
|
+
**Problem:** move state between components — notify an ancestor, message a
|
|
4
|
+
specific component, or run async work and fold in the result.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
// bubble — walk toward the root; the first ancestor with the handler runs
|
|
8
|
+
input: { onItemClick(ctx) { ctx.bubble("itemSelected", [this]); return this; } },
|
|
9
|
+
bubble: { itemSelected(item, ctx) { return this.insertInLogAt(0, item.label); } },
|
|
10
|
+
|
|
11
|
+
// send / receive — deliver to one target (self, or ctx.at.<step> for another)
|
|
12
|
+
methods: { submit(ctx) { ctx.at.field("status").send("flash", [this.draft]); return this; } },
|
|
13
|
+
receive: { flash(message, ctx) { return this.setMessage(message); } },
|
|
14
|
+
|
|
15
|
+
// request / response — async host work, result routed back
|
|
16
|
+
receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
|
|
17
|
+
response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pick by direction: **bubble** for aggregate state an ancestor owns (logs,
|
|
21
|
+
selections); **send/receive** to address one known component
|
|
22
|
+
(`ctx.at.field("x")` / `.index(name, i)` / `.key(name, k)`, default self);
|
|
23
|
+
**request/response** for fetch/timer/IndexedDB — register the async fn with
|
|
24
|
+
`scope.registerRequestHandlers({...})`, and `response` gets `(res, err)`. `ctx`
|
|
25
|
+
is always the trailing arg. `receive.init` is a convention, not a lifecycle
|
|
26
|
+
hook — dispatch it with `app.sendAtRoot("init")`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Edit through a dynamic target
|
|
2
|
+
|
|
3
|
+
**Problem:** render a value owned by a distant ancestor *and* let edits made in
|
|
4
|
+
the child land back on the owner — without forwarding events up by hand.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
// producer exposes a field (or a seq-access) as a dynamic
|
|
8
|
+
const Workspace = component({
|
|
9
|
+
name: "Workspace",
|
|
10
|
+
fields: { sheet: null },
|
|
11
|
+
provide: { active: ".sheet" }, // or ".items[.selectedKey]"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// a distant consumer renders it as a target
|
|
15
|
+
const Toolbar = component({
|
|
16
|
+
name: "Toolbar",
|
|
17
|
+
lookup: { active: { for: "Workspace.active", default: ".missing" } },
|
|
18
|
+
view: html`<x render="*active" as="edit"></x>`,
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Because `*active` resolves to a real **path** (not a copied value), the event
|
|
23
|
+
fired inside the rendered child is *teleported*: the mutation skips the
|
|
24
|
+
intermediate components and lands on `Workspace.sheet`, so the owner and any
|
|
25
|
+
other view of the same value update in lock-step. A `provide` can even point at
|
|
26
|
+
a seq-access (`.items[.selectedKey]`) to expose "the selected item". This is
|
|
27
|
+
the **edit** counterpart of the share-state-without-prop-drilling recipe.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Enrich each item
|
|
2
|
+
|
|
3
|
+
**Problem:** show a value derived from each item (a count, a formatted label)
|
|
4
|
+
without storing it on the data.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<li @each=".items" @enrich-with="enrichItem">
|
|
8
|
+
<x text="@value"></x> (<x text="@count"></x> characters)
|
|
9
|
+
</li>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
alter: {
|
|
14
|
+
enrichItem(binds, _key, item) {
|
|
15
|
+
binds.count = item.length; // becomes @count in the template
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`@enrich-with` receives a **mutable** `binds` object (seeded with `{ key,
|
|
21
|
+
value }`); every key you set becomes an `@`-prefixed binding for that item's
|
|
22
|
+
subtree. The return value is ignored. Combine freely with `@when` and
|
|
23
|
+
`@loop-with` on the same element. Without an `@each` on the same element,
|
|
24
|
+
`@enrich-with` enriches the whole scope instead (see the bind-text-and-attributes
|
|
25
|
+
recipe).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Filter a list
|
|
2
|
+
|
|
3
|
+
**Problem:** render only the items that match a condition.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<li @each=".items" @when="filterItem">
|
|
7
|
+
<span @text="@key"></span>: <x text="@value"></x>
|
|
8
|
+
</li>
|
|
9
|
+
<!-- on <x render-each> the prefix drops: when="filterItem" -->
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
alter: {
|
|
14
|
+
filterItem(_key, item) {
|
|
15
|
+
return item.toLowerCase().includes(this.query.toLowerCase());
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`@when` names an `alter` handler called per item as `(key, value, iterData)`;
|
|
21
|
+
return `false` to skip. It filters *after* any `@loop-with` slice, so a page
|
|
22
|
+
can yield fewer than its window. Filtering reads other fields off `this`
|
|
23
|
+
directly (`this.query`) — there are no paths in the template.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Handle events
|
|
2
|
+
|
|
3
|
+
**Problem:** respond to a DOM event and update state.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<button @on.click="$inc">+</button> <!-- $ calls a method -->
|
|
7
|
+
<button @on.click="dec">-</button> <!-- bare name = input handler -->
|
|
8
|
+
|
|
9
|
+
<!-- pass args by name; ctx is auto-appended last -->
|
|
10
|
+
<input @on.input="$setStr value" />
|
|
11
|
+
<input @on.input="$setN valueAsInt" />
|
|
12
|
+
<button @on.click="$addItem JsonSelector">+</button>
|
|
13
|
+
|
|
14
|
+
<!-- modifiers: keydown +send (Enter) / +cancel (Esc), and +ctrl/+cmd/+alt -->
|
|
15
|
+
<input @on.keydown+send="$submit value" @on.keydown+cancel="$reset" />
|
|
16
|
+
|
|
17
|
+
<!-- custom elements: any CustomEvent reaches @on.<name>, detail is `value` -->
|
|
18
|
+
<emoji-picker @on.emoji-click="onPick value"></emoji-picker>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Handlers return a (new) instance of `this`. The first slot is a handler name
|
|
22
|
+
(`$method`, or a bare name in `input`/`alter`); later slots are built-in arg
|
|
23
|
+
names — `value`, `valueAsInt`/`valueAsFloat`, `event`, `key`, `isAlt`,
|
|
24
|
+
`isShift`, `isCtrl`/`isCmd`, `dragInfo`, … `value` resolves to
|
|
25
|
+
`event.target.value` (or `.checked` for a checkbox, or `event.detail` for a
|
|
26
|
+
`CustomEvent`). Bind events declaratively with `@on.` rather than reaching for
|
|
27
|
+
the node and `addEventListener` — an outside listener bypasses the transactor.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Iterate a list
|
|
2
|
+
|
|
3
|
+
**Problem:** render one element per item in a list/map field.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<!-- a host element per item: @key and @value are bound in the loop -->
|
|
7
|
+
<li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
|
|
8
|
+
|
|
9
|
+
<!-- a child component per item -->
|
|
10
|
+
<x render-each=".items"></x>
|
|
11
|
+
<div @each=".items"><x render-it></x></div>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@each` accepts a `.field` or a `*dynamic` (not a `$method` — a method result
|
|
15
|
+
has no addressable path for event dispatch). `@key`/`@value` are auto-bound on
|
|
16
|
+
host-element loops; under `render-each` / `render-it` each item is rendered as
|
|
17
|
+
its own component (no `@value`). Use `render-each` for lists of components,
|
|
18
|
+
`@each` for plain values.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Paginate a list
|
|
2
|
+
|
|
3
|
+
**Problem:** show one page at a time without iterating or rendering the
|
|
4
|
+
off-page items.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<li @each=".items" @loop-with="paginate">
|
|
8
|
+
<span class="badge" @text="@key"></span> <x text="@value"></x>
|
|
9
|
+
</li>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
fields: { items: [], page: 0, pageSize: 5 },
|
|
14
|
+
alter: {
|
|
15
|
+
paginate(seq) { // runs once per render, before iteration
|
|
16
|
+
const start = this.page * this.pageSize;
|
|
17
|
+
return { iterData: { total: seq.size }, start, end: start + this.pageSize };
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`@loop-with` returns `{ iterData?, start?, end? }`, all optional. `start`/`end`
|
|
23
|
+
slice with `Array.prototype.slice` semantics (`end` exclusive, negatives count
|
|
24
|
+
from the end). Slicing is positional but **preserves each item's original
|
|
25
|
+
key** — `@key` is the index in the full list, so events and two-way binding
|
|
26
|
+
keep their identity across pages. `iterData` is the shared per-loop value
|
|
27
|
+
handed to `@when` / `@enrich-with`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Render a child component
|
|
2
|
+
|
|
3
|
+
**Problem:** a component holds another component in a field and wants to render
|
|
4
|
+
it (reaching into nested data is not allowed — `@text=".child.name"` fails).
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
fields: { greeting: Greeting.make({ name: "world" }) },
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<x render=".greeting"></x> <!-- default ("main") view -->
|
|
12
|
+
<x render=".greeting" as="edit"></x> <!-- a named view -->
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The child draws its own view from its own fields, so inside `Greeting`'s view
|
|
16
|
+
`@text=".name"` reads the child's `name`. This is the idiomatic way to display
|
|
17
|
+
nested structure: make the nested thing a component and render it, rather than
|
|
18
|
+
trying to path into it. For a list of children use `render-each` (see the
|
|
19
|
+
iterate-a-list recipe); to flip which view renders, see the switch-between-views
|
|
20
|
+
recipe.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Reuse markup with macros
|
|
2
|
+
|
|
3
|
+
**Problem:** the same markup fragment repeats across a view and you want one
|
|
4
|
+
definition — but it has no state of its own.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import { macro, html } from "tutuca";
|
|
8
|
+
|
|
9
|
+
const badge = macro(
|
|
10
|
+
{ label: "'New'", kind: "'info'" }, // defaults are *expressions*
|
|
11
|
+
html`<span :class="$'badge badge-{^kind}'" @text="^label"></span>`,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const card = macro(
|
|
15
|
+
{ title: "'Card'" },
|
|
16
|
+
html`<div class="card"><h2 @text="^title"></h2><x:slot></x:slot></div>`,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export function getMacros() { return { badge, card }; }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<x:badge></x:badge> <!-- defaults -->
|
|
24
|
+
<x:badge label="Sale"></x:badge> <!-- static string (no quotes needed) -->
|
|
25
|
+
<x:badge :label=".status"></x:badge> <!-- bind a field -->
|
|
26
|
+
<x:card title="Hi"><p>body</p></x:card> <!-- children fill <x:slot> -->
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A macro is pure template expansion — no fields, no handlers. Parameters are
|
|
30
|
+
read as `^name`; calls inside the body (`$method`, `.field`) resolve against
|
|
31
|
+
the *host* component. `<x:slot>` (or `<x:slot name="…">` for named slots)
|
|
32
|
+
receives the caller's children. Register with `scope.registerMacros(...)`;
|
|
33
|
+
registry keys are lowercased (`<x:Card>` → `card`). For repeated markup that
|
|
34
|
+
*does* need state, use a child component instead (see the render-a-child-component
|
|
35
|
+
recipe).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Share state without prop-drilling
|
|
2
|
+
|
|
3
|
+
**Problem:** a deep descendant needs a value owned by a distant ancestor, and
|
|
4
|
+
you don't want to thread it through every component in between.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
// producer — exposes one of its fields under a name
|
|
8
|
+
const Producer = component({
|
|
9
|
+
name: "EntryEditorAndSelector",
|
|
10
|
+
fields: { items: [] },
|
|
11
|
+
provide: { entries: ".items" },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// consumer — forwards to the producer's binding by "Component.name"
|
|
15
|
+
const Consumer = component({
|
|
16
|
+
name: "Selector",
|
|
17
|
+
lookup: { entries: { for: "EntryEditorAndSelector.entries", default: ".items" } },
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!-- read the dynamic with the * prefix — iterate or render it -->
|
|
23
|
+
<option @each="*entries" :value="@value" @text="@label"></option>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`provide` publishes a field under a name; a descendant's `lookup` resolves
|
|
27
|
+
`*name` to the nearest matching producer, falling back to `default` when none
|
|
28
|
+
is in scope. `*name` works wherever a `.field` does for iteration/rendering.
|
|
29
|
+
This is the **read** side; to edit the producer's value through the dynamic,
|
|
30
|
+
see the edit-through-a-dynamic-target recipe.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Show or hide content
|
|
2
|
+
|
|
3
|
+
**Problem:** render an element only when a condition holds.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<div @show=".isOpen">Details</div>
|
|
7
|
+
<p @hide=".isOpen">(hidden when open)</p>
|
|
8
|
+
|
|
9
|
+
<!-- boolean predicates for one-field checks -->
|
|
10
|
+
<p @show="empty? .items">No results</p>
|
|
11
|
+
<p @show="truthy? .query">Searching…</p>
|
|
12
|
+
<div @show="equals? .view 'detail'">detail view</div>
|
|
13
|
+
|
|
14
|
+
<!-- on an <x> render op: wraps the produced node, no extra DOM element -->
|
|
15
|
+
<x text=".count" show=".isOpen"></x>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The closed set of predicates is `empty?`, `truthy?`, `falsy?`, `null?`,
|
|
19
|
+
`equals?` (binary). For a condition spanning multiple fields, use a no-arg
|
|
20
|
+
method instead (`@show="$canSubmit"`). `@show`/`@hide` toggle visibility on a
|
|
21
|
+
host element; the wrapper form (`show=` / `hide=` on `<x>`) conditionally
|
|
22
|
+
emits the node with no surrounding element.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Switch between views
|
|
2
|
+
|
|
3
|
+
**Problem:** render the *same* component in a different view (e.g. a read-only
|
|
4
|
+
"main" vs an "edit" form).
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
component({
|
|
8
|
+
view: html`<p @text=".title"></p>`, // "main"
|
|
9
|
+
views: { edit: html`<input :value=".title" @on.input="$setTitle value" />` },
|
|
10
|
+
});
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<!-- as= picks the view for one <x render> element only -->
|
|
15
|
+
<x render=".value"></x>
|
|
16
|
+
<x render=".value" as="edit"></x>
|
|
17
|
+
|
|
18
|
+
<!-- @push-view forces a view on every component rendered under the host -->
|
|
19
|
+
<div @push-view=".view"><x render-each=".items"></x></div>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`as="edit"` applies to the direct component only and falls back to `main` if
|
|
23
|
+
the view is absent. `@push-view` pushes a view name onto the render stack so
|
|
24
|
+
every descendant picks the first matching view (else `main`) — use it to flip a
|
|
25
|
+
whole subtree (e.g. a list) into edit mode at once. To toggle *sibling panels*
|
|
26
|
+
by a field instead, see the tabbed-interface recipe.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Tabbed interface
|
|
2
|
+
|
|
3
|
+
**Problem:** build tabs — a single `currentView` field decides which panel
|
|
4
|
+
shows, and the active tab button is highlighted.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<div role="tablist" class="tabs">
|
|
8
|
+
<button
|
|
9
|
+
role="tab"
|
|
10
|
+
@if.class="equals? .currentView 'overview'"
|
|
11
|
+
@then="'tab tab-active'"
|
|
12
|
+
@else="'tab'"
|
|
13
|
+
@on.click="$setCurrentView 'overview'"
|
|
14
|
+
>Overview</button>
|
|
15
|
+
<button
|
|
16
|
+
role="tab"
|
|
17
|
+
@if.class="equals? .currentView 'pricing'"
|
|
18
|
+
@then="'tab tab-active'"
|
|
19
|
+
@else="'tab'"
|
|
20
|
+
@on.click="$setCurrentView 'pricing'"
|
|
21
|
+
>Pricing</button>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div @show="equals? .currentView 'overview'">…overview…</div>
|
|
25
|
+
<div @show="equals? .currentView 'pricing'">…pricing…</div>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
fields: { currentView: "overview" }, // $setCurrentView is auto-generated
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
One string field is the whole state machine. `equals? .currentView 'overview'`
|
|
33
|
+
drives both the panel's `@show` and the active-tab class via `@if.class` /
|
|
34
|
+
`@then` / `@else`. Tab clicks call the auto-generated setter with a
|
|
35
|
+
string-literal arg (`@on.click="$setCurrentView 'pricing'"`). This toggles
|
|
36
|
+
**sibling panels** by predicate; to swap a *component's own* rendered view
|
|
37
|
+
instead, see the switch-between-views recipe. The field name is yours to pick
|
|
38
|
+
(`tab`, `currentView`, …).
|