tutuca 0.9.80 → 0.9.82

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.80",
3
+ "version": "0.9.82",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -28,6 +28,7 @@ When authoring tutuca code, also load these if available:
28
28
  | Task | File |
29
29
  | ---------------------------------------------------------------------------------------------- | ------------------------------- |
30
30
  | Authoring `component({...})`, `html`...`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
31
+ | Designing components — responsibilities, state ownership, channel choice, do's & don'ts | [component-design.md](./component-design.md) |
31
32
  | CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
32
33
  | `bubble` / `send`-`receive` / async `request`-`response` channels, `$unknown`, request-handler registration | [request-response.md](./request-response.md) |
33
34
  | Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
@@ -42,9 +42,16 @@ Touch is wired up too (drag fires after a small move threshold).
42
42
 
43
43
  ## Dynamic Bindings
44
44
 
45
- For passing values "context-style" through nested components without prop
46
- drilling. **`provide`** on the producer; **`lookup`** on consumers;
47
- resolve as `*name`.
45
+ For passing values "context-style" to a deep descendant without threading
46
+ them through every component in between. **`provide`** on the producer;
47
+ **`lookup`** on consumers; resolve as `*name`.
48
+
49
+ > **Best practice:** keep state local to the component and reach for
50
+ > `provide` / `lookup` only when it is genuinely the only solution. Dynamic
51
+ > bindings couple a consumer to a producer that may not be in scope — prefer
52
+ > keeping components as self-contained as possible: let a child render the
53
+ > field it needs from its owner, and lift state only as far up the tree as it
54
+ > actually needs to live.
48
55
 
49
56
  ```js
50
57
  const Theme = component({
@@ -0,0 +1,151 @@
1
+ # Tutuca Cheatsheet — Component Design
2
+
3
+ How to *shape* a feature into one or more tutuca components — responsibilities,
4
+ where state lives, which channel to reach for — before you reach for syntax. The
5
+ mechanics live elsewhere: component skeleton, fields, directives, and the
6
+ post-edit verification recipe in [core.md](./core.md); the orchestration channels
7
+ in [request-response.md](./request-response.md); dynamic bindings (`provide` /
8
+ `lookup` / `*x`) in [advanced.md](./advanced.md); task recipes in
9
+ [patterns/README.md](./patterns/README.md). This file is a router with judgment
10
+ attached — every rule points at its canonical home rather than restating it.
11
+ Read it when deciding how to split a feature into components, where a piece of
12
+ state should live, how two components should talk, or when reviewing a component
13
+ for design smells.
14
+
15
+ ## Decide in this order
16
+
17
+ Walk these top-down whenever you add or reshape a component:
18
+
19
+ 1. **What single responsibility is this?** If the answer has an "and" in it, split
20
+ it into separate components.
21
+ 2. **Who owns each piece of state?** The component that reads and mutates a value
22
+ owns it. Put the field there; let others render or message it.
23
+ 3. **How do these components talk?** Pick the narrowest channel that reaches the
24
+ owner — see the ladder below.
25
+ 4. **Where does the outside world cross the boundary?** Outbound I/O goes through
26
+ `ctx.request` / `response`; inbound external events go through
27
+ `app.sendAtRoot` to the root. Keep the logic inside tutuca on both sides.
28
+
29
+ ## Communication decision ladder
30
+
31
+ Reach for the *narrowest* channel that does the job, and only move further down
32
+ the ladder when the one above can't express it:
33
+
34
+ - **The component owns the state needed to respond** → call a plain **`method`** on
35
+ itself (stays self-contained). See [core.md](./core.md) Methods.
36
+ - **An ancestor owns aggregate state** (a log, a selection, a total) → **`ctx.bubble`**
37
+ up toward the root; the first ancestor with a matching handler runs. See
38
+ [request-response.md](./request-response.md) "When to bubble".
39
+ - **You need to reach one known component** → **`ctx.send` / `receive`**, addressing a
40
+ specific path with `ctx.at` (defaults to self). See
41
+ [request-response.md](./request-response.md) "When to send".
42
+ - **The work is async or host-side** (fetch, timer, IndexedDB, an SDK) →
43
+ **`ctx.request` / `response`**, which goes out to the host and routes the result
44
+ back into component state. See [request-response.md](./request-response.md).
45
+ - **An external event pushes *into* the app** (WebSocket, `postMessage`, …) →
46
+ **`app.sendAtRoot`**, which lands the inbound event on the root. See
47
+ [request-response.md](./request-response.md) "Integrating with the outside world".
48
+ - **A deep descendant needs a value owned far away** and nothing in between should
49
+ know about it → **`provide` / `lookup` (`*name`)** across the tree — the last
50
+ resort. See [advanced.md](./advanced.md).
51
+
52
+ A compact worked version of the first four (`method`, `bubble`, `send`/`receive`,
53
+ `request`/`response`) lives in
54
+ [patterns/coordinate-components.md](./patterns/coordinate-components.md).
55
+
56
+ ## Do's & Don'ts
57
+
58
+ - **Do create single-purpose components. Don't pack multiple responsibilities
59
+ into one.** A component that draws its own view from its own fields is the unit
60
+ of reuse and the unit of testing. → [patterns/render-a-child-component.md](./patterns/render-a-child-component.md)
61
+
62
+ - **Don't add a `kind` / `type` field and branch the view on it. Do make one
63
+ component per kind and render it with `<x render=".item">`.** Conditional-on-kind
64
+ views grow into tangled `@if` chains; a component per kind keeps each view flat
65
+ and each concern isolated. This is also why pathing into nested data is barred —
66
+ model the nested thing as a component instead. → [core.md](./core.md) "Common
67
+ pitfalls" (Paths are not allowed in values) and
68
+ [patterns/render-a-child-component.md](./patterns/render-a-child-component.md)
69
+
70
+ - **Do keep state in the component that owns and uses it, and lift it only as far
71
+ up the tree as it needs to live. Don't thread a value through every component in
72
+ between** (the React "prop drilling" reflex) — let a child render the field it
73
+ needs from its owner. → [patterns/share-state-across-the-tree.md](./patterns/share-state-across-the-tree.md)
74
+
75
+ - **Do reach for `provide` / `lookup` (`*name`) last** — only when a deep
76
+ descendant needs a value owned far away and nothing in between should know about
77
+ it. Dynamic bindings couple a consumer to a producer that may not be in scope.
78
+ → [advanced.md](./advanced.md)
79
+
80
+ - **Do pick the channel by direction (the ladder above). Don't `bubble` an event
81
+ no ancestor consumes, and don't `send` to self when a plain method call would
82
+ do.** `bubble` emits an *event* any ancestor can observe; `send` delivers a
83
+ *message* to one target. → [request-response.md](./request-response.md) "When to
84
+ bubble" / "When to send"
85
+
86
+ - **Do keep logic inside the tutuca app when integrating with the outside world.**
87
+ Route outbound work through `ctx.request` / `response` and inbound external
88
+ events through `app.sendAtRoot` to the root (which forwards deeper with `ctx.at`),
89
+ so handlers stay the single owner of state changes. **Don't mutate `app.state`
90
+ directly or `addEventListener` outside the model** — state changed that way
91
+ bypasses the immutable `return this.set…()` discipline and is invisible to the
92
+ component that owns it. → [request-response.md](./request-response.md)
93
+ "Integrating with the outside world" (and its ⚠️ note)
94
+
95
+ - **Do handle every DOM event with tutuca's built-in `@on.` handlers — including
96
+ custom events fired by web components.** `@on.click`, `@on.input`,
97
+ `@on.<custom-event>` (the event `detail` surfaces as `value`) keep the event
98
+ inside the model, so it flows through a handler and returns a new `this`. **Don't
99
+ reach in from the outside with `addEventListener`** — a listener attached out of
100
+ band mutates state the owning component can't see and bypasses the transactor.
101
+ → [core.md](./core.md) "Event Handling" and "Web Components & Custom Events"
102
+
103
+ - **Do use inline predicates and auto-generated mutators. Don't hand-write
104
+ `is*Selected()` / `select*()` boilerplate.** A single field plus
105
+ `equals? .activeSection 'todo'` / `empty?` and the generated `$setActiveSection`
106
+ / `toggleX` often *is* the whole state machine. → [core.md](./core.md) "Methods
107
+ as Predicates & Computed Values" and "Field Types & Auto-generated API"
108
+
109
+ - **Do remember a rendered child gets a clean namespace.** Parent `@` bindings
110
+ (`@each`, `@enrich-with`) don't cross a `<x render>` boundary — pass a value
111
+ across it with `*name`, not by assuming the binding leaks in. → [advanced.md](./advanced.md)
112
+
113
+ - **Do add a decoy view when a margaui class is assembled at runtime.** The margaui
114
+ compiler only scans `class=` / `:class=` *literals*, so classes that appear only
115
+ in `@if.class` `@then`/`@else` payloads or are composed dynamically in an
116
+ `@enrich-with` / `:class` handler emit no CSS and render unstyled. Add a hidden,
117
+ never-rendered view (e.g. `_margauiClasses: html\`<p class="btn-success btn-ghost
118
+ …"></p>\``) listing each such class as a real literal so the scanner picks it up.
119
+ → [advanced.md](./advanced.md) "Pitfall: `@if.class` payloads are invisible to
120
+ the scanner", and the worked decoy view in `personal-site.js`.
121
+
122
+ - **Do close the loop after every change** with `tutuca <module> lint` → `test` →
123
+ `render`. → [core.md](./core.md) "Verifying changes"
124
+
125
+ ## Smells & refactors
126
+
127
+ - **`is*Selected()` + `select*()` methods → predicate + generated setter.**
128
+ Replace `@on.click="selectTodo"` / `@show="$isTodoSelected"` with
129
+ `@on.click="$setActiveSection 'todo'"` / `@show="equals? .activeSection 'todo'"`,
130
+ derive the current value from one field. (See `composability.js`, commit `808574e`.)
131
+ - **A view that `@if`-branches on a `kind` field → one component per kind**, each
132
+ rendered with `<x render>`.
133
+ - **A value passed down through three components that don't use it → move the
134
+ state up to the nearest common owner** and let the leaf render it directly; only
135
+ if nothing in between should know it, use `provide` / `lookup`.
136
+ - **Host code poking `app.state` or attaching a listener → an `app.sendAtRoot`
137
+ handler on the root**, with the mutation expressed as `return this.set…()`.
138
+
139
+ ## See also
140
+
141
+ - [core.md](./core.md) — component skeleton, fields, directives, predicates, the
142
+ verification recipe, and the "Common pitfalls" list.
143
+ - [request-response.md](./request-response.md) — the channels in depth
144
+ (`bubble`, `send`/`receive`, `request`/`response`, `sendAtRoot`) and integrating
145
+ with the outside world.
146
+ - [advanced.md](./advanced.md) — `provide` / `lookup` / `*name` and the
147
+ clean-namespace boundary.
148
+ - [patterns/coordinate-components.md](./patterns/coordinate-components.md),
149
+ [patterns/share-state-across-the-tree.md](./patterns/share-state-across-the-tree.md),
150
+ [patterns/render-a-child-component.md](./patterns/render-a-child-component.md) —
151
+ runnable recipes for the rules above.
@@ -22,6 +22,9 @@ the `tutuca` CLI.
22
22
  > the task touches them. Task-oriented "how do I do X" recipes (iteration,
23
23
  > filtering, slicing, conditional content, conditional attributes, dynamic
24
24
  > vars, composition, events, …): see [patterns/README.md](./patterns/README.md).
25
+ > Design judgment — shaping a feature into components, where state lives, which
26
+ > channel to reach for, and a do's & don'ts list: see
27
+ > [component-design.md](./component-design.md).
25
28
 
26
29
  ## Verifying changes
27
30
 
@@ -976,6 +979,9 @@ export function getTests({ describe, test, expect }) { /*...*/ } // optiona
976
979
 
977
980
  ## See also
978
981
 
982
+ - [component-design.md](./component-design.md) — design judgment for shaping a
983
+ feature into components: responsibilities, where state lives, which channel to
984
+ reach for, and a curated do's & don'ts list.
979
985
  - [request-response.md](./request-response.md) — `bubble` / `send`-`receive` /
980
986
  `request`-`response` channels, the `ctx.at` `PathBuilder`, `$unknown`, and
981
987
  request-handler registration.
@@ -24,7 +24,7 @@ task.
24
24
 
25
25
  ## Context & dynamic variables
26
26
 
27
- - [Share state without prop-drilling](share-state-without-prop-drilling.md) — `provide` / `lookup` and reading `*name`.
27
+ - [Share state across the tree](share-state-across-the-tree.md) — `provide` / `lookup` and reading `*name`.
28
28
  - [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and teleport edits back to the owner.
29
29
 
30
30
  ## Composition
@@ -24,4 +24,4 @@ fired inside the rendered child is *teleported*: the mutation skips the
24
24
  intermediate components and lands on `Workspace.sheet`, so the owner and any
25
25
  other view of the same value update in lock-step. A `provide` can even point at
26
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.
27
+ the **edit** counterpart of the share-state-across-the-tree recipe.
@@ -1,8 +1,16 @@
1
- # Share state without prop-drilling
1
+ # Share state across the tree
2
2
 
3
3
  **Problem:** a deep descendant needs a value owned by a distant ancestor, and
4
4
  you don't want to thread it through every component in between.
5
5
 
6
+ > **Reach for this last.** Keep state local to the component and use
7
+ > `provide` / `lookup` only when it is genuinely the only solution — a value
8
+ > owned far away that a deep descendant needs and nothing in between should
9
+ > know about. Dynamic bindings couple a consumer to a producer that may not be
10
+ > in scope, so keep components as self-contained as possible: let a child
11
+ > render the field it needs from its owner, and lift state only as far up the
12
+ > tree as it needs to live.
13
+
6
14
  ```js
7
15
  // producer — exposes one of its fields under a name
8
16
  const Producer = component({