@madojs/mado 0.6.1 → 0.8.0

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/AGENTS.md CHANGED
@@ -91,6 +91,36 @@ component("x-timer", (ctx) => {
91
91
 
92
92
  **`resource()`, `effect()`, and subscriptions inside `setup()` hook into the lifecycle automatically** — no need to write onDispose for them.
93
93
 
94
+ ### 4b. Reactive attributes — `ctx.attr()`
95
+
96
+ ```ts
97
+ // ❌ NO (reading once, never reactive)
98
+ component("x-badge", ({ host }) => () => {
99
+ const variant = host.getAttribute("variant") ?? "default";
100
+ return html`<span class=${variant}>...</span>`;
101
+ });
102
+
103
+ // ❌ NO (MutationObserver boilerplate)
104
+ component("x-badge", ({ host, onDispose }) => {
105
+ const variant = signal(host.getAttribute("variant") ?? "default");
106
+ const obs = new MutationObserver(() =>
107
+ variant.set(host.getAttribute("variant") ?? "default"),
108
+ );
109
+ obs.observe(host, { attributes: true, attributeFilter: ["variant"] });
110
+ onDispose(() => obs.disconnect());
111
+ return () => html`<span class=${variant}>...</span>`;
112
+ });
113
+
114
+ // ✅ YES — one line, reactive, no cleanup needed
115
+ component("x-badge", ({ attr }) => {
116
+ const variant = attr("variant", "default");
117
+ return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
118
+ });
119
+ ```
120
+
121
+ `ctx.attr(name, defaultValue?)` returns a `Signal<string>` that auto-updates.
122
+ The attribute is automatically added to `observedAttributes`.
123
+
94
124
  ### 5. Reactive value in template child position = function
95
125
 
96
126
  The most common AI mistake:
@@ -99,13 +129,13 @@ The most common AI mistake:
99
129
  const count = signal(0);
100
130
 
101
131
  // ❌ NOT REACTIVE — count() is read once
102
- html`<div>${count() * 2}</div>`
132
+ html`<div>${count() * 2}</div>`;
103
133
 
104
134
  // ✅ REACTIVE — the function will be called when count changes
105
- html`<div>${() => count() * 2}</div>`
135
+ html`<div>${() => count() * 2}</div>`;
106
136
 
107
137
  // ✅ ALSO OK — the signal itself is a function, Mado recognizes it
108
- html`<div>${count}</div>`
138
+ html`<div>${count}</div>`;
109
139
  ```
110
140
 
111
141
  **Rule of thumb:** if there is a signal call (with parentheses) inside `${...}`, wrap it in `() => ...`.
@@ -114,17 +144,17 @@ html`<div>${count}</div>`
114
144
 
115
145
  ```ts
116
146
  // string/number → attribute
117
- html`<a href=${url}>...</a>`
147
+ html`<a href=${url}>...</a>`;
118
148
 
119
149
  // DOM property (objects, numbers without serialization, .value for input)
120
- html`<input .value=${user.name}>`
121
- html`<my-list .items=${arr}>`
150
+ html`<input .value=${user.name} />`;
151
+ html`<my-list .items=${arr}></my-list>`;
122
152
 
123
153
  // boolean attribute (toggle)
124
- html`<button ?disabled=${isLoading}>...</button>`
154
+ html`<button ?disabled=${isLoading}>...</button>`;
125
155
 
126
156
  // event
127
- html`<button @click=${fn}>...</button>`
157
+ html`<button @click=${fn}>...</button>`;
128
158
  ```
129
159
 
130
160
  Common mistake: `disabled=${loading()}` — this attempts to set a **string** attribute `disabled="true"` or `disabled="false"`, which does not work correctly. **Use `?disabled=`.**
@@ -135,10 +165,19 @@ Common mistake: `disabled=${loading()}` — this attempts to set a **string** at
135
165
  import { each } from "@madojs/mado";
136
166
 
137
167
  // ❌ Works, but no keyed reconciliation → loses focus on reorder
138
- html`<ul>${() => items().map(t => html`<li>${t.name}</li>`)}</ul>`
168
+ html`<ul>
169
+ ${() => items().map((t) => html`<li>${t.name}</li>`)}
170
+ </ul>`;
139
171
 
140
172
  // ✅ Correct: keyed, reuses DOM nodes
141
- html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
173
+ html`<ul>
174
+ ${() =>
175
+ each(
176
+ items(),
177
+ (t) => t.id,
178
+ (t) => html`<li>${t.name}</li>`,
179
+ )}
180
+ </ul>`;
142
181
  ```
143
182
 
144
183
  ### 8. Routing — `routes()` + `page()`
@@ -198,14 +237,20 @@ import { useForm } from "@madojs/mado";
198
237
 
199
238
  const f = useForm({
200
239
  email: { required: true, type: "email" },
201
- age: { required: true, type: "number", min: 18 },
240
+ age: { required: true, type: "number", min: 18 },
202
241
  });
203
242
 
204
243
  html`
205
- <form @submit=${f.onSubmit(async v => { await api.save(v); })}>
206
- <input name="email" @input=${f.onInput} @blur=${f.onBlur}>
207
- ${() => f.touched().email && f.errors().email
208
- ? html`<small>${f.errors().email}</small>` : null}
244
+ <form
245
+ @submit=${f.onSubmit(async (v) => {
246
+ await api.save(v);
247
+ })}
248
+ >
249
+ <input name="email" @input=${f.onInput} @blur=${f.onBlur} />
250
+ ${() =>
251
+ f.touched().email && f.errors().email
252
+ ? html`<small>${f.errors().email}</small>`
253
+ : null}
209
254
  <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
210
255
  </form>
211
256
  `;
@@ -220,16 +265,23 @@ import { component, css, html } from "@madojs/mado";
220
265
 
221
266
  component("x-card", () => () => html`<div><slot></slot></div>`, {
222
267
  styles: css`
223
- :host { display: block; padding: 1rem; }
224
- div { background: var(--bg); }
225
- ::slotted(h2) { margin: 0; }
268
+ :host {
269
+ display: block;
270
+ padding: 1rem;
271
+ }
272
+ div {
273
+ background: var(--bg);
274
+ }
275
+ ::slotted(h2) {
276
+ margin: 0;
277
+ }
226
278
  `,
227
279
  });
228
280
 
229
281
  // Light DOM (without Shadow), global styles:
230
282
  component("x-shell", () => () => html`...`, {
231
- shadow: false, // disables Shadow DOM
232
- styles: css`x-shell header { ... }`, // selectors are written as usual
283
+ shadow: false, // disables Shadow DOM
284
+ styles: css`x-shell header { ... }`, // selectors are written as usual
233
285
  });
234
286
  ```
235
287
 
@@ -314,17 +366,17 @@ When generating an app, prefer the blessed production shape from
314
366
 
315
367
  ## Where to find specific answers
316
368
 
317
- | Question | File |
318
- |---|---|
319
- | How does reactivity work? | `src/signal.ts` (283 lines) |
320
- | How are templates parsed? | `src/html.ts` (1013 lines) |
321
- | How does the router work? | `src/router.ts` (~530 lines) |
322
- | How does resource + cache work? | `src/resource.ts` (297 lines) |
323
- | How do forms work? | `src/forms.ts` (212 lines) |
369
+ | Question | File |
370
+ | -------------------------------- | -------------------------------- |
371
+ | How does reactivity work? | `src/signal.ts` (283 lines) |
372
+ | How are templates parsed? | `src/html.ts` (1013 lines) |
373
+ | How does the router work? | `src/router.ts` (~530 lines) |
374
+ | How does resource + cache work? | `src/resource.ts` (297 lines) |
375
+ | How do forms work? | `src/forms.ts` (212 lines) |
324
376
  | How should an app be structured? | `docs/en/10-app-architecture.md` |
325
- | How should errors be handled? | `docs/en/15-error-handling.md` |
326
- | How should bake be used? | `docs/en/16-bake-cookbook.md` |
327
- | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
377
+ | How should errors be handled? | `docs/en/15-error-handling.md` |
378
+ | How should bake be used? | `docs/en/16-bake-cookbook.md` |
379
+ | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
328
380
 
329
381
  ## Before committing
330
382
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,140 @@
4
4
 
5
5
  Nothing yet.
6
6
 
7
+ ## 0.8.0
8
+
9
+ Core reliability fixes from "Pulse" stress-test (Round 2): Kanban 210 cards,
10
+ Gantt 500-task computed chain, rapid navigation, field arrays with server
11
+ populate. Three critical issues found and resolved.
12
+
13
+ ### Fixed
14
+
15
+ - **`ctx.attr()` — MutationObserver fallback.** `observedAttributes` is read
16
+ once at `customElements.define()` time. Attributes registered via `ctx.attr()`
17
+ inside `setup()` were too late for the browser's `attributeChangedCallback`.
18
+ Now a single `MutationObserver` per instance covers all `ctx.attr()` attributes
19
+ and auto-disconnects on component removal. This was a silent failure — the
20
+ signal read the initial value correctly but never updated on external changes
21
+ like `?disabled=${() => !form.isValid()}`.
22
+
23
+ - **`useForm().array().items()` — reactive reads.** The internal `read()`
24
+ function used `values.peek()` (untracked) instead of `values()`. Effects and
25
+ templates calling `items()` never re-ran when the array changed via
26
+ `append()` / `replace()` / `remove()`. Field arrays populated from server
27
+ data showed empty lists. One-line fix: `values.peek()` → `values()`.
28
+
29
+ ### Added
30
+
31
+ - **`onDispose` in `PageContext`.** `page()` view now receives `onDispose(fn)` —
32
+ tied to the same lifecycle as `resource()` / `effect()` but for manual
33
+ subscriptions (`setInterval`, WebSocket, EventSource) that aren't auto-managed.
34
+ Cleaned up automatically on navigation.
35
+
36
+ ```ts
37
+ export default page({
38
+ view: ({ onDispose }) => {
39
+ const id = setInterval(pollInbox, 3000);
40
+ onDispose(() => clearInterval(id));
41
+ return html`...`;
42
+ },
43
+ });
44
+ ```
45
+
46
+ - **`ctx.attr()` regression test** confirming that external `setAttribute()`
47
+ after `connectedCallback` correctly updates the reactive signal via the
48
+ MutationObserver fallback path.
49
+
50
+ ### Notes
51
+
52
+ - Core reactivity engine passes all stress checks: diamond dependency (500
53
+ tasks, 1 recompute per batch), rapid navigation (20× board↔issue, 0 broken
54
+ states), `persisted()` cross-tab sync via BroadcastChannel.
55
+ - Bundle size for a full Pulse app (8 pages, kanban, gantt, inbox, settings):
56
+ **36.7 KB gzip** / 31.8 KB brotli.
57
+ - 141 tests, 138 pass, 0 fail, 3 skipped (browser-only).
58
+
59
+ ## 0.7.0
60
+
61
+ Reactive component props, Shadow DOM + Forms fixes, deterministic releases,
62
+ and `mado serve` unification. Motivated by stress-test findings in a real-world
63
+ admin panel (see `MADO_TEST_REPORT.md`).
64
+
65
+ ### Breaking Changes
66
+
67
+ - **`mado serve` in app-mode** no longer uses the legacy `serveStaticProject()`
68
+ fallback. It now always goes through `server/serve.mjs`, which means
69
+ `--host`, `--port`, `mado.config.json` dev.proxy, and HMR all work for
70
+ generated apps. If you relied on the old no-HMR behaviour, pass
71
+ `NO_HMR=1 mado serve`.
72
+
73
+ ### Added — Framework
74
+
75
+ - **`ctx.attr(name, defaultValue?)`** — reactive attribute accessor for
76
+ components. Returns a `Signal<string>` that auto-updates when the attribute
77
+ changes on the host element via `attributeChangedCallback`. No more
78
+ `MutationObserver` boilerplate in every component.
79
+
80
+ ```ts
81
+ component("x-badge", ({ attr }) => {
82
+ const variant = attr("variant", "default");
83
+ return () =>
84
+ html`<span class=${() => `badge-${variant()}`}><slot></slot></span>`;
85
+ });
86
+ ```
87
+
88
+ Attributes used with `ctx.attr()` are automatically added to
89
+ `observedAttributes`.
90
+
91
+ ### Added — Starters
92
+
93
+ - **`apiFetcher<T>()`** in `starters/admin/src/lib/api.ts` — a fetcher for
94
+ `resource()` that attaches the Bearer token from memory. Use for protected
95
+ endpoints instead of the plain `jsonFetcher()`.
96
+ - **`x-button`**: now bridges Shadow DOM → Light DOM form submit via
97
+ `form.requestSubmit()`. Buttons inside Shadow DOM cannot natively trigger
98
+ `<form>` submit in Light DOM — this is now handled automatically.
99
+ - **`x-button`**: uses `ctx.attr("disabled")` for reactive disabled state.
100
+ External `?disabled=${() => !form.isValid()}` now correctly enables/disables
101
+ the inner button.
102
+ - **`x-input`**: proxies `.name` and `.value` DOM properties on the host
103
+ element so that `useForm().onInput` works after Shadow DOM event retargeting.
104
+
105
+ ### Added — CLI / Build
106
+
107
+ - **`mado release --no-clean`**: release now cleans the entire `out/` directory
108
+ before building (deterministic artifacts). Pass `--no-clean` to opt out.
109
+ Previously stale assets, removed bake routes, and deleted public files could
110
+ linger in the deploy artifact.
111
+ - **`scripts/bake.mjs`**: `<title>` now falls back to `page.title` if
112
+ `head().title` is not explicitly set. Previously baked HTML kept the template
113
+ `<title>` from `index.html` — a critical SEO gap.
114
+
115
+ ### Added — Documentation
116
+
117
+ - **`docs/en/17-shadow-dom-forms.md`** — full recipe for using `useForm()` with
118
+ Shadow DOM components (proxy properties, form submit bridge, ctx.attr()).
119
+ - **`llms.txt`**: added `ctx.attr()` section, `apiFetcher` recipe, and Shadow
120
+ DOM + Forms guidance.
121
+
122
+ ### Fixed
123
+
124
+ - **`x-button` in starters**: the disabled state was read once from
125
+ `host.hasAttribute("disabled")` in the render function — never updating when
126
+ the attribute changed externally. Every form using `?disabled` on `x-button`
127
+ was broken from the start.
128
+ - **`x-input` in starters**: `useForm().onInput` received `undefined` for
129
+ `name` and `value` because Shadow DOM retargets `e.target` from the inner
130
+ `<input>` to `<x-input>`, which had no DOM properties.
131
+ - **`jsonFetcher()`**: the admin starter relied on `jsonFetcher()` for protected
132
+ endpoints but it sends no Authorization header. Documented the pattern and
133
+ added `apiFetcher()`.
134
+ - **`mado serve`**: app-mode did not respect `--host`, `--port`, or config
135
+ settings. All flag pass-through now goes through `server/serve.mjs`.
136
+ - **`mado release`**: stale files from deleted bake routes or removed public
137
+ assets could remain in `out/`. Now cleans `out/` fully before building.
138
+ - **`mado bake`**: `<title>` was not set in baked HTML if only `page.title`
139
+ was defined (without `head().title`).
140
+
7
141
  ## 0.6.1
8
142
 
9
143
  Starter & release-pipeline hardening pass. No public API breaks.
@@ -12,6 +146,7 @@ starter / bundle / bake / dev-server contour. All fixes verified by
12
146
  regression tests added in this release.
13
147
 
14
148
  ### Fixed
149
+
15
150
  - **Starters**: every `index.html` in `starters/{admin,crud,minimal}/` now
16
151
  uses root-absolute paths in the importmap and entry `<script>` tag
17
152
  (`/node_modules/@madojs/mado/...`, `/dist/main.js`). Relative paths
@@ -20,7 +155,7 @@ regression tests added in this release.
20
155
  `/admin/orders/dist/main.js` → 404 → blank page). Inline comments in each
21
156
  file explain the trap so it does not get reverted.
22
157
  - **Starters/admin**: `pages/admin/order-detail.ts` now uses `each(items,
23
- key, render)` instead of `o.items.map(...)`, matching `llms.txt` rule #3
158
+ key, render)` instead of `o.items.map(...)`, matching `llms.txt` rule #3
24
159
  and the framework's own pitfalls documentation.
25
160
  - **`scripts/bundle.mjs`**: cleans stale hashed assets before every build.
26
161
  Previously each `mado bundle` / `mado release` left old `main-<hash>.js`
@@ -57,6 +192,7 @@ regression tests added in this release.
57
192
  had succeeded.
58
193
 
59
194
  ### Added
195
+
60
196
  - **`mado dev` / `mado serve` flag pass-through**: `cli.mjs` now splits
61
197
  positional arguments from flags via `splitDevArgs()`, so calls like
62
198
  `mado dev --host 127.0.0.1`, `mado dev showcase --port 6000` and
@@ -71,7 +207,7 @@ regression tests added in this release.
71
207
  - **`scripts/bake.mjs`**: fails loudly when the manifest exists but no
72
208
  page declares `bake: { paths, data }`. The previous behaviour produced
73
209
  `0 pages + sitemap.xml` silently with exit code 0, making `mado
74
- release` look successful while shipping only the SPA shell with no
210
+ release` look successful while shipping only the SPA shell with no
75
211
  SEO-friendly HTML. The new warning prints the skipped routes, a
76
212
  worked example bake snippet, and exits non-zero. Override with
77
213
  `MADO_BAKE_ALLOW_EMPTY=1` for intentional SPA-only deploys.
@@ -99,12 +235,14 @@ regression tests added in this release.
99
235
  response.
100
236
 
101
237
  ### Changed
238
+
102
239
  - **`server/serve.mjs`** default host is now `localhost` (was implicitly
103
240
  `0.0.0.0`). LAN exposure is opt-in via `mado dev --host 0.0.0.0` or
104
241
  `HOST=0.0.0.0`. The startup banner shows both the bound host and a
105
242
  click-friendly URL (`localhost` substituted when bound to `0.0.0.0`).
106
243
 
107
244
  ### Notes
245
+
108
246
  - No public API changes; no migrations required. Apps that previously
109
247
  worked on a fresh-out-of-the-box `mado init` did so only because
110
248
  someone manually fixed the starter's relative paths and dev deps —
@@ -124,12 +262,13 @@ pipeline, core hardening and v1 recipe docs.
124
262
  Phase 1 — Repo-vs-app split:
125
263
 
126
264
  ### Added
265
+
127
266
  - `MADO_V1_PLAN.md` — executable tracker for the v1 push.
128
267
  - `scripts/_config.mjs` — single configuration loader (defaults < `mado.config.json`
129
268
  < CLI flags). Exports `loadConfig`, `detectContext`, `parseFlags`,
130
269
  `resolveProjectPath`. [v1 F1.1]
131
270
  - `mado release` command: one-shot `typecheck + build + bundle + bake + copy
132
- public/ → out/` pipeline so apps have exactly one command to ship. [v1 F1.3]
271
+ public/ → out/` pipeline so apps have exactly one command to ship. [v1 F1.3]
133
272
  - `mado.config.json` shipped in the `minimal` and `crud` starters with the
134
273
  default app-mode layout (`src/routes.ts`, `index.html`, `out/`). [v1 F1.4]
135
274
  - Tests: `test/config-loader.test.mjs`, `test/bake-cli.test.mjs` (11 + 3
@@ -137,6 +276,7 @@ Phase 1 — Repo-vs-app split:
137
276
  flags, and the no-more-silent-`[object Object]` contract). [v1 F1.6]
138
277
 
139
278
  ### Changed
279
+
140
280
  - `scripts/bake.mjs` now reads configuration from `mado.config.json` and
141
281
  accepts `--entry`, `--template`, `--out`, `--base-url` flags. In app-mode
142
282
  defaults are `src/routes.ts` + `index.html` + `out/baked/`; the
@@ -158,6 +298,7 @@ Phase 1 — Repo-vs-app split:
158
298
  Phase 2 — One blessed way:
159
299
 
160
300
  ### Added
301
+
161
302
  - `layout()` factory in `src/page.ts` (alias of `nested()`) plus `Guard` and
162
303
  `GuardResult` types. Exported from the public API. [v1 F2.1 / F2.3]
163
304
  - Route guards: nested groups and individual pages accept `guard: Guard | Guard[]`.
@@ -182,6 +323,7 @@ Phase 2 — One blessed way:
182
323
  Phase 3 — Bake first-class + Release pipeline:
183
324
 
184
325
  ### Added
326
+
185
327
  - `mado release` writes `_redirects` (`/* /index.html 200`) and `_headers`
186
328
  (immutable for `/assets/*`, no-cache for HTML) into `out/` when they do not
187
329
  exist, so Cloudflare Pages / Netlify deploys "just work". [v1 F3.7]
@@ -195,6 +337,7 @@ Phase 3 — Bake first-class + Release pipeline:
195
337
  and copied `public/` assets are all present. [v1 F3.10]
196
338
 
197
339
  ### Changed
340
+
198
341
  - `scripts/preview.mjs` now reads `mado.config.json` (`build.out`, `dev.port`),
199
342
  refuses to auto-build by default in app-mode, and asks the user to run
200
343
  `mado release` first. Legacy auto-build is opt-in via `PREVIEW_AUTOBUILD=1`
@@ -209,6 +352,7 @@ Phase 3 — Bake first-class + Release pipeline:
209
352
  dependencies. The startup banner prints the active proxy table. [v1 F3.6]
210
353
 
211
354
  ### Deferred to v0.7
355
+
212
356
  - `mado dev` does not yet serve baked routes inline. Workaround: run
213
357
  `mado release && mado preview`. [v1 F3.2]
214
358
  - `mado check` (bake-safety scan over `bake:` routes) is not exposed yet.
@@ -218,6 +362,7 @@ Phase 3 — Bake first-class + Release pipeline:
218
362
  Phase 4 — Core hardening:
219
363
 
220
364
  ### Added
365
+
221
366
  - `computed(fn, { equals })` option to suppress subscriber reruns when an
222
367
  observed computed recomputes to an equal value. [v1 F4.3]
223
368
  - HTML directives: `unsafeHTML()`, `ref()`, `classMap()` and `styleMap()` are
@@ -239,6 +384,7 @@ Phase 4 — Core hardening:
239
384
  its regression tests. [v1 F4.9]
240
385
 
241
386
  ### Changed
387
+
242
388
  - `computed()` now releases dependency subscriptions after unobserved reads and
243
389
  after the last subscriber is disposed, avoiding long-lived stale subscriptions
244
390
  in the signal graph. [v1 F4.1]
@@ -257,6 +403,7 @@ Phase 4 — Core hardening:
257
403
  Phase 5 — Documentation:
258
404
 
259
405
  ### Added
406
+
260
407
  - `docs/en/10-app-architecture.md`, `14-testing.md`, `15-error-handling.md`
261
408
  and `16-bake-cookbook.md` complete the v1 English recipe set. [v1 F5.1-F5.4]
262
409
  - `AGENTS.md` now includes an "App architecture for LLM" section and `llms.txt`
@@ -15,13 +15,25 @@
15
15
  * Shadow DOM (open) is used by default. It can be disabled, and
16
16
  * styles will be scoped via @scope (or a tag-prefix fallback).
17
17
  */
18
- import { type Disposer } from "./signal.js";
18
+ import { type Signal, type Disposer } from "./signal.js";
19
19
  import { html, type TemplateResult } from "./html.js";
20
20
  import { type CSSResult } from "./css.js";
21
21
  export interface ComponentContext {
22
22
  host: HTMLElement;
23
23
  /** Run cleanup when the component is removed. */
24
24
  onDispose(fn: Disposer): void;
25
+ /**
26
+ * Reactive attribute accessor. Returns a Signal<string> that updates
27
+ * automatically whenever the attribute changes on the host element.
28
+ *
29
+ * const variant = ctx.attr("variant", "primary");
30
+ * return () => html`<div class=${variant()}>…</div>`;
31
+ *
32
+ * No MutationObserver boilerplate needed — the signal updates via
33
+ * attributeChangedCallback. The attribute name is automatically added to
34
+ * observedAttributes if not already listed.
35
+ */
36
+ attr(name: string, defaultValue?: string): Signal<string>;
25
37
  }
26
38
  export type SetupFn = (ctx: ComponentContext) => () => TemplateResult;
27
39
  export type StyleInput = string | CSSResult | Array<string | CSSResult>;
@@ -38,9 +50,10 @@ export interface ComponentOptions {
38
50
  /**
39
51
  * List of observed attributes.
40
52
  *
41
- * v0.3: this is plain reflection into host[attr], without a reactive props API.
42
- * If you need to re-render the component when an attribute changes, create a signal()
43
- * inside setup() and update it manually from your own wrapper/event.
53
+ * Attributes listed here are reflected to host[attr] via
54
+ * attributeChangedCallback and also power ctx.attr() reactive signals.
55
+ * You only need to list them here if you use the legacy property reflection
56
+ * pattern. ctx.attr() automatically registers any attribute it tracks.
44
57
  */
45
58
  observedAttributes?: readonly string[];
46
59
  }
@@ -15,10 +15,10 @@
15
15
  * Shadow DOM (open) is used by default. It can be disabled, and
16
16
  * styles will be scoped via @scope (or a tag-prefix fallback).
17
17
  */
18
- import { effect } from "./signal.js";
18
+ import { signal, effect } from "./signal.js";
19
19
  import { html, render } from "./html.js";
20
20
  import { adopt, scopeStyles } from "./css.js";
21
- import { createLifecycle, runInLifecycle } from "./lifecycle.js";
21
+ import { createLifecycle, runInLifecycle, } from "./lifecycle.js";
22
22
  import { warnOnce } from "./diagnostics.js";
23
23
  export function component(tagName, setup, options = {}) {
24
24
  if (!tagName.includes("-")) {
@@ -36,18 +36,22 @@ export function component(tagName, setup, options = {}) {
36
36
  }
37
37
  const useShadow = options.shadow !== false;
38
38
  const observed = options.observedAttributes ?? [];
39
+ // Collect attribute names that ctx.attr() will track. These are merged with
40
+ // options.observedAttributes to form the final static observedAttributes.
41
+ const attrSignalNames = new Set(observed);
39
42
  // Normalize styles to an array of CSSStyleSheet once.
40
43
  // Sheets are shared across all instances — memory is not duplicated.
41
44
  const stylesheets = normalizeStyles(options.styles, tagName, useShadow);
42
45
  class MadoElement extends HTMLElement {
43
46
  static get observedAttributes() {
44
- return [...observed];
47
+ return [...attrSignalNames];
45
48
  }
46
49
  #root;
47
50
  #renderer = null;
48
51
  #effectDispose = null;
49
52
  #lifecycle = null;
50
53
  #connected = false;
54
+ #attrSignals = new Map();
51
55
  constructor() {
52
56
  super();
53
57
  this.#root = useShadow ? this.attachShadow({ mode: "open" }) : this;
@@ -69,14 +73,44 @@ export function component(tagName, setup, options = {}) {
69
73
  // getCurrentLifecycle() and register its own cleanup.
70
74
  const lifecycle = createLifecycle();
71
75
  this.#lifecycle = lifecycle;
76
+ const host = this;
72
77
  const ctx = {
73
78
  host: this,
74
79
  // ctx.onDispose proxies to lifecycle — the single source of truth
75
80
  // for component cleanups (including auto-cleanup from
76
81
  // resource(), navigator listeners, etc.).
77
82
  onDispose: (fn) => lifecycle.onDispose(fn),
83
+ attr(name, defaultValue = "") {
84
+ let s = host.#attrSignals.get(name);
85
+ if (!s) {
86
+ s = signal(host.getAttribute(name) ?? defaultValue);
87
+ host.#attrSignals.set(name, s);
88
+ // Record for future instances (HMR) — helps observedAttributes
89
+ // on hot-reloaded re-defines.
90
+ attrSignalNames.add(name);
91
+ }
92
+ return s;
93
+ },
78
94
  };
79
95
  this.#renderer = runInLifecycle(lifecycle, () => setup(ctx));
96
+ // After setup(), install a single MutationObserver for all attrs
97
+ // registered via ctx.attr(). This is necessary because
98
+ // observedAttributes is read once at customElements.define() time —
99
+ // attrs added by ctx.attr() during setup are too late for the
100
+ // browser's attributeChangedCallback mechanism. The observer bridges
101
+ // the gap for the current and all future instances.
102
+ if (this.#attrSignals.size > 0) {
103
+ const attrNames = [...this.#attrSignals.keys()];
104
+ const obs = new MutationObserver((mutations) => {
105
+ for (const m of mutations) {
106
+ const s = this.#attrSignals.get(m.attributeName);
107
+ if (s)
108
+ s.set(this.getAttribute(m.attributeName) ?? "");
109
+ }
110
+ });
111
+ obs.observe(this, { attributes: true, attributeFilter: attrNames });
112
+ lifecycle.onDispose(() => obs.disconnect());
113
+ }
80
114
  this.#effectDispose = effect(() => {
81
115
  render(this.#renderer(), this.#root);
82
116
  });
@@ -89,7 +123,12 @@ export function component(tagName, setup, options = {}) {
89
123
  this.#connected = false;
90
124
  }
91
125
  attributeChangedCallback(name, _old, value) {
92
- // reflect attribute to property user can bind a signal to it
126
+ // Update ctx.attr() signal if it exists for this attribute.
127
+ const s = this.#attrSignals.get(name);
128
+ if (s) {
129
+ s.set(value ?? "");
130
+ }
131
+ // Legacy reflection: reflect attribute to property.
93
132
  this[name] = value;
94
133
  }
95
134
  }
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"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"}
package/dist/src/forms.js CHANGED
@@ -277,7 +277,10 @@ export function useForm(schema, options = {}) {
277
277
  validate: validateAll,
278
278
  array(name) {
279
279
  const read = () => {
280
- const value = getPath(values.peek(), name);
280
+ // Use values() (tracked) — not values.peek() — so that effects and
281
+ // computed() that call items() re-run when the array changes via
282
+ // append/remove/replace/etc.
283
+ const value = getPath(values(), name);
281
284
  return Array.isArray(value) ? value : [];
282
285
  };
283
286
  const write = (items) => {