@async/framework 0.2.2 → 0.4.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/CHANGELOG.md +26 -0
- package/README.md +324 -48
- package/examples/cache/index.html +3 -3
- package/examples/components/main.js +10 -10
- package/examples/counter/index.html +2 -2
- package/examples/partials/index.html +2 -2
- package/examples/product/index.html +9 -9
- package/examples/router/index.html +2 -2
- package/examples/router/main.js +2 -2
- package/examples/server-call/index.html +2 -2
- package/examples/ssr/index.html +1 -1
- package/examples/ssr/main.js +2 -2
- package/examples/streaming/index.html +2 -2
- package/examples/streaming/main.js +2 -2
- package/framework.js +3912 -0
- package/package.json +14 -3
- package/src/app.js +73 -53
- package/src/attributes.js +52 -0
- package/src/cache.js +31 -16
- package/src/component.js +94 -19
- package/src/handlers.js +24 -5
- package/src/html.js +99 -6
- package/src/index.js +2 -0
- package/src/loader.js +291 -54
- package/src/partials.js +26 -11
- package/src/registry-store.js +257 -0
- package/src/router.js +42 -3
- package/src/server.js +12 -4
- package/src/signals.js +32 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-06-17
|
|
4
|
+
|
|
5
|
+
- Added a generated root `framework.js` ESM bundle for UNPKG browser imports.
|
|
6
|
+
- Expanded the README with Async layer definitions and an htmx comparison.
|
|
7
|
+
- Added `on:attach` as the canonical component attach lifecycle pseudo-event
|
|
8
|
+
with `on:mount` kept as a compatibility alias.
|
|
9
|
+
- Added top-level `class:*` bindings, including aggregate `class:`
|
|
10
|
+
string/object/array class sets.
|
|
11
|
+
- Added inline `html` template bindings for signal refs, class arrays/objects,
|
|
12
|
+
`value="${signalRef}"`, and generated component handlers via
|
|
13
|
+
`this.handler(fn)`.
|
|
14
|
+
- Added generated component-local signals via `this.signal(initial)`.
|
|
15
|
+
|
|
16
|
+
## 0.3.0 - 2026-06-17
|
|
17
|
+
|
|
18
|
+
- Added a shared registry store behind `Async`, app runtimes, and concrete
|
|
19
|
+
registries so apps can inspect signals, handlers, server ids, routes,
|
|
20
|
+
partials, components, and split cache state from one place.
|
|
21
|
+
- Added configurable HTML attribute prefixes, with `async:*`, `signal:*`, and
|
|
22
|
+
`on:*` as the defaults plus explicit support for `data-async-*`,
|
|
23
|
+
`data-signal-*`, and `data-on-*`.
|
|
24
|
+
- Declared the UNPKG package entry explicitly so the package root can be used as
|
|
25
|
+
a no-build browser ESM CDN import.
|
|
26
|
+
- Documented the UNPKG import-map setup for importing `@async/framework` by
|
|
27
|
+
package name in no-build browser apps.
|
|
28
|
+
|
|
3
29
|
## 0.2.2 - 2026-06-17
|
|
4
30
|
|
|
5
31
|
- Fixed release doctor validation to accept the pipeline-generated GitHub
|
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# @async/framework
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Async is a layered framework plan that starts as a no-build browser bootloader:
|
|
4
|
+
signals, async signals, delegated command events, scoped fragment components,
|
|
5
|
+
server calls, route partials, and out-of-order boundary swaps without a virtual
|
|
6
|
+
DOM.
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
pnpm add @async/framework
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
```html
|
|
13
|
-
<main
|
|
13
|
+
<main async:container>
|
|
14
14
|
<button type="button" on:click="decrement">-</button>
|
|
15
|
-
<strong
|
|
15
|
+
<strong signal:text="count"></strong>
|
|
16
16
|
<button type="button" on:click="increment">+</button>
|
|
17
17
|
</main>
|
|
18
18
|
<script type="module" src="./main.js"></script>
|
|
@@ -43,10 +43,10 @@ Async.start({ root: document });
|
|
|
43
43
|
|
|
44
44
|
## What It Is
|
|
45
45
|
|
|
46
|
-
`@async/framework` is the
|
|
47
|
-
runtime small and explicit:
|
|
46
|
+
`@async/framework` is the Layer 1 runtime plus the first Layer 2 app/server
|
|
47
|
+
primitives. It keeps the runtime small and explicit:
|
|
48
48
|
|
|
49
|
-
- No build step for consumers.
|
|
49
|
+
- No build step for Layer 1 consumers.
|
|
50
50
|
- No virtual DOM, diff path, hydration runtime, or component rerender loop.
|
|
51
51
|
- Signals are the state boundary.
|
|
52
52
|
- `Async.use(...)` registers app declarations before or after startup.
|
|
@@ -57,8 +57,27 @@ runtime small and explicit:
|
|
|
57
57
|
- Boundaries can be swapped out of order and rescanned, which keeps server
|
|
58
58
|
streaming and partial HTML replacement simple.
|
|
59
59
|
|
|
60
|
-
Higher layers can
|
|
61
|
-
|
|
60
|
+
Higher layers can add JSX lowering, TypeScript, chunk manifests, compiler-owned
|
|
61
|
+
server/client splits, and intent-first authoring later. They should compile down
|
|
62
|
+
to the same runtime registries and HTML protocol.
|
|
63
|
+
|
|
64
|
+
## Layers
|
|
65
|
+
|
|
66
|
+
Async is designed as layers, so each level can stay useful without forcing the
|
|
67
|
+
next level on every app.
|
|
68
|
+
|
|
69
|
+
| Layer | Name | Requirement | Purpose |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| 1 | Runtime bootloader | No build. CDN or direct ESM import. | Signals, async signals, handlers, command events, lifecycle pseudo-events, scoped fragments, and boundary swaps. |
|
|
72
|
+
| 2 | App/server layer | Light server integration. No app compiler required. | `Async.use(...)`, router modes, server function proxy, partial registry, SSR output, browser activation, and split browser/server cache. |
|
|
73
|
+
| 3 | Authoring build | Build step required. | JSX, ESM, and TypeScript authoring that lowers into Layer 1 HTML attributes and Layer 2 registries. |
|
|
74
|
+
| 4 | Chunk and resumability metadata | Build metadata required. | Lazy module manifests, visibility/prefetch hints, resource graphs, and resumability records that the bootloader can consume. |
|
|
75
|
+
| 5 | Framework compiler | Compiler required. | Server/client partitioning, code motion, optimized registry generation, serialized closures, and deeper resumability transforms. |
|
|
76
|
+
| 6 | TSRX and intent layer | Higher-level compiler required. | More declarative author intent, AI/compiler-friendly metadata, and source forms that generate lower-layer Async apps. |
|
|
77
|
+
|
|
78
|
+
The package in this repository intentionally focuses on Layers 1 and 2. Layers
|
|
79
|
+
3 through 6 are higher authoring surfaces, not extra runtime requirements for
|
|
80
|
+
plain HTML apps.
|
|
62
81
|
|
|
63
82
|
## Install
|
|
64
83
|
|
|
@@ -69,12 +88,78 @@ pnpm add @async/framework
|
|
|
69
88
|
The package is ESM-only and supports Node.js 24 and newer for tests, examples,
|
|
70
89
|
and package lifecycle tooling. Browser consumers import ESM directly.
|
|
71
90
|
|
|
91
|
+
## CDN
|
|
92
|
+
|
|
93
|
+
The package ships a root `framework.js` ESM bundle for UNPKG and can be loaded
|
|
94
|
+
without a build step. Use `@latest` for quick prototypes, and pin an exact
|
|
95
|
+
version in production:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<main async:container>
|
|
99
|
+
<button type="button" on:click="increment">+</button>
|
|
100
|
+
<strong signal:text="count"></strong>
|
|
101
|
+
</main>
|
|
102
|
+
|
|
103
|
+
<script type="module">
|
|
104
|
+
import {
|
|
105
|
+
Async,
|
|
106
|
+
createSignal
|
|
107
|
+
} from "https://unpkg.com/@async/framework@latest/framework.js";
|
|
108
|
+
|
|
109
|
+
Async.use({
|
|
110
|
+
signal: {
|
|
111
|
+
count: createSignal(0)
|
|
112
|
+
},
|
|
113
|
+
handler: {
|
|
114
|
+
increment() {
|
|
115
|
+
this.signals.update("count", (count) => count + 1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
Async.start({ root: document });
|
|
121
|
+
</script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
You can also use an import map so app code imports `@async/framework` by name:
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<script type="importmap">
|
|
128
|
+
{
|
|
129
|
+
"imports": {
|
|
130
|
+
"@async/framework": "https://unpkg.com/@async/framework@latest/framework.js"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<script type="module">
|
|
136
|
+
import {
|
|
137
|
+
Async,
|
|
138
|
+
createSignal
|
|
139
|
+
} from "@async/framework";
|
|
140
|
+
|
|
141
|
+
Async.use({
|
|
142
|
+
signal: {
|
|
143
|
+
count: createSignal(0)
|
|
144
|
+
},
|
|
145
|
+
handler: {
|
|
146
|
+
increment() {
|
|
147
|
+
this.signals.update("count", (count) => count + 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
Async.start({ root: document });
|
|
153
|
+
</script>
|
|
154
|
+
```
|
|
155
|
+
|
|
72
156
|
## Core API
|
|
73
157
|
|
|
74
158
|
```js
|
|
75
159
|
import {
|
|
76
160
|
AsyncLoader,
|
|
77
161
|
Async,
|
|
162
|
+
attributeName,
|
|
78
163
|
asyncSignal,
|
|
79
164
|
createApp,
|
|
80
165
|
createCacheRegistry,
|
|
@@ -84,11 +169,13 @@ import {
|
|
|
84
169
|
createSignal,
|
|
85
170
|
createHandlerRegistry,
|
|
86
171
|
createPartialRegistry,
|
|
172
|
+
createRegistryStore,
|
|
87
173
|
createRouteRegistry,
|
|
88
174
|
createRouter,
|
|
89
175
|
createServerProxy,
|
|
90
176
|
createServerRegistry,
|
|
91
177
|
createSignalRegistry,
|
|
178
|
+
defineAttributeConfig,
|
|
92
179
|
defineApp,
|
|
93
180
|
defineCache,
|
|
94
181
|
defineComponent,
|
|
@@ -170,6 +257,50 @@ Naming rules:
|
|
|
170
257
|
Singular registry keys are canonical: `signal`, `handler`, `server`,
|
|
171
258
|
`partial`, `route`, `component`, and nested `cache.browser` / `cache.server`.
|
|
172
259
|
|
|
260
|
+
### Registry Inspection
|
|
261
|
+
|
|
262
|
+
`Async.registry` is the global inspection surface for registered app pieces.
|
|
263
|
+
Every runtime and concrete registry also points at the same backing store:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
Async.registry.keys("signal");
|
|
267
|
+
Async.registry.entries("route");
|
|
268
|
+
Async.registry.snapshot();
|
|
269
|
+
|
|
270
|
+
const runtime = Async.start({ root: document });
|
|
271
|
+
|
|
272
|
+
runtime.registry.keys("handler");
|
|
273
|
+
runtime.signals.registry === runtime.registry;
|
|
274
|
+
runtime.browser.cache.registry === runtime.registry;
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Supported inspection types:
|
|
278
|
+
|
|
279
|
+
```txt
|
|
280
|
+
signal
|
|
281
|
+
handler
|
|
282
|
+
server
|
|
283
|
+
partial
|
|
284
|
+
route
|
|
285
|
+
component
|
|
286
|
+
cache.browser
|
|
287
|
+
cache.server
|
|
288
|
+
cache.browser.entries
|
|
289
|
+
cache.server.entries
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Browser runtime inspection exposes server ids as descriptors, not executable
|
|
293
|
+
server functions, and does not expose server cache contents:
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
runtime.registry.keys("server");
|
|
297
|
+
runtime.registry.get("server", "products.get");
|
|
298
|
+
// { id: "products.get", kind: "server" }
|
|
299
|
+
|
|
300
|
+
runtime.registry.snapshot().entries.server;
|
|
301
|
+
// {}
|
|
302
|
+
```
|
|
303
|
+
|
|
173
304
|
### Signals
|
|
174
305
|
|
|
175
306
|
```js
|
|
@@ -249,37 +380,130 @@ AsyncLoader scans regular HTML attributes:
|
|
|
249
380
|
|
|
250
381
|
| Attribute | Behavior |
|
|
251
382
|
| --- | --- |
|
|
252
|
-
| `
|
|
383
|
+
| `async:container` | Marks a scannable app root |
|
|
253
384
|
| `on:click="selectProduct"` | Delegated command event |
|
|
254
385
|
| `on:submit="preventDefault; save"` | Sequential command chain |
|
|
255
386
|
| `on:click="server.cart.add(productId)"` | Server command with signal args |
|
|
256
|
-
| `
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
387
|
+
| `on:attach="setup"` | Component root attach lifecycle pseudo-event |
|
|
388
|
+
| `on:visible="trackView"` | Component root visible lifecycle pseudo-event |
|
|
389
|
+
| `signal:text="product.title"` | Text binding |
|
|
390
|
+
| `signal:value="productId"` | Form value binding with writeback |
|
|
391
|
+
| `signal:attr:disabled="product.$loading"` | Attribute binding |
|
|
392
|
+
| `class:selected="selected"` | Class toggle from a signal path |
|
|
393
|
+
| `signal:class="buttonClasses"` | Class set from a signal value: string, object, or array |
|
|
394
|
+
| `async:boundary="product"` | Async or streamed replacement boundary |
|
|
395
|
+
| `async:loading="product"` | Boundary loading template |
|
|
396
|
+
| `async:ready="product"` | Boundary ready template |
|
|
397
|
+
| `async:error="product"` | Boundary error template |
|
|
264
398
|
|
|
265
399
|
```html
|
|
266
|
-
<section
|
|
267
|
-
<template
|
|
400
|
+
<section async:boundary="product">
|
|
401
|
+
<template async:loading="product">
|
|
268
402
|
<p>Loading...</p>
|
|
269
403
|
</template>
|
|
270
|
-
<template
|
|
271
|
-
<h1
|
|
404
|
+
<template async:ready="product">
|
|
405
|
+
<h1 signal:text="product.title"></h1>
|
|
272
406
|
</template>
|
|
273
|
-
<template
|
|
274
|
-
<p
|
|
407
|
+
<template async:error="product">
|
|
408
|
+
<p signal:text="product.$error.message"></p>
|
|
275
409
|
</template>
|
|
276
410
|
</section>
|
|
277
411
|
```
|
|
278
412
|
|
|
413
|
+
The default prefixes are `async:`, `signal:`, and `on:`. You can switch to
|
|
414
|
+
data attributes when a host needs that shape:
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
Async.start({
|
|
418
|
+
root: document,
|
|
419
|
+
attributes: {
|
|
420
|
+
async: "data-async-",
|
|
421
|
+
class: "data-class-",
|
|
422
|
+
signal: "data-signal-",
|
|
423
|
+
on: "data-on-"
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
That maps to `data-async-container`, `data-on-click="save"`,
|
|
429
|
+
`data-signal-text="product.title"`, and `data-class-selected="selected"`.
|
|
430
|
+
|
|
431
|
+
Named class toggles use their own top-level namespace:
|
|
432
|
+
|
|
433
|
+
```html
|
|
434
|
+
<button
|
|
435
|
+
class="button"
|
|
436
|
+
class:selected="selected"
|
|
437
|
+
>
|
|
438
|
+
Add
|
|
439
|
+
</button>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Aggregate class binding uses `signal:class`. It reads the current signal value
|
|
443
|
+
and accepts strings, objects, and arrays:
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
Async.use({
|
|
447
|
+
signal: {
|
|
448
|
+
buttonClasses: createSignal([
|
|
449
|
+
"button-primary",
|
|
450
|
+
{ selected: true, disabled: false },
|
|
451
|
+
["compact"]
|
|
452
|
+
])
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```html
|
|
458
|
+
<button signal:class="buttonClasses">Add</button>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Inside `html` templates, `signal:class` can also receive objects or arrays
|
|
462
|
+
directly. Signal refs inside the object or array are tracked:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
const selected = this.signal("selected", false);
|
|
466
|
+
const tone = this.signal("tone", "primary");
|
|
467
|
+
|
|
468
|
+
return html`
|
|
469
|
+
<article signal:class="${["card", tone, { selected }]}"}>
|
|
470
|
+
...
|
|
471
|
+
</article>
|
|
472
|
+
`;
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
For component-local state that does not need a stable public id, omit the name.
|
|
476
|
+
The signal is still registered under the component scope:
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
const selected = this.signal(false);
|
|
480
|
+
const tone = this.signal("primary");
|
|
481
|
+
|
|
482
|
+
return html`
|
|
483
|
+
<article signal:class="${["card", selected, tone]}">
|
|
484
|
+
...
|
|
485
|
+
</article>
|
|
486
|
+
`;
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`value="${signalRef}"` in an `html` template is equivalent to adding
|
|
490
|
+
`signal:value` for that signal. It writes back on input/change:
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
const productId = this.signal("productId", "sku-1");
|
|
494
|
+
|
|
495
|
+
return html`<input value="${productId}">`;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`signal:class:selected="selected"` remains supported as a compatibility alias,
|
|
499
|
+
but new examples should use `class:selected`. The parser-safe top-level
|
|
500
|
+
aggregate form `class:="buttonClasses"` also remains supported.
|
|
501
|
+
|
|
279
502
|
### Command Events
|
|
280
503
|
|
|
281
|
-
`on:*` works with any native DOM event name. `on:
|
|
282
|
-
reserved pseudo-events with cleanup support.
|
|
504
|
+
`on:*` works with any native DOM event name. `on:attach` and `on:visible` are
|
|
505
|
+
reserved component lifecycle pseudo-events with cleanup support. `on:mount`
|
|
506
|
+
remains as a compatibility alias for `on:attach`.
|
|
283
507
|
|
|
284
508
|
Command chains use semicolons and are awaited sequentially:
|
|
285
509
|
|
|
@@ -417,13 +641,13 @@ Router modes:
|
|
|
417
641
|
CSR startup can use an empty route boundary:
|
|
418
642
|
|
|
419
643
|
```html
|
|
420
|
-
<main
|
|
644
|
+
<main async:container>
|
|
421
645
|
<nav>
|
|
422
646
|
<a href="/">Home</a>
|
|
423
647
|
<a href="/products/sku-1">Product</a>
|
|
424
648
|
</nav>
|
|
425
649
|
|
|
426
|
-
<section
|
|
650
|
+
<section async:boundary="route"></section>
|
|
427
651
|
</main>
|
|
428
652
|
```
|
|
429
653
|
|
|
@@ -520,10 +744,10 @@ const response = await serverRuntime.render("/products/123");
|
|
|
520
744
|
The returned HTML includes a route boundary plus a JSON snapshot:
|
|
521
745
|
|
|
522
746
|
```html
|
|
523
|
-
<section
|
|
747
|
+
<section async:boundary="route">
|
|
524
748
|
<!-- server-rendered route partial -->
|
|
525
749
|
</section>
|
|
526
|
-
<script type="application/json"
|
|
750
|
+
<script type="application/json" async:snapshot>{}</script>
|
|
527
751
|
```
|
|
528
752
|
|
|
529
753
|
Browser activation scans the existing HTML and attaches events. It does not
|
|
@@ -545,21 +769,25 @@ type and no rerender loop.
|
|
|
545
769
|
|
|
546
770
|
```js
|
|
547
771
|
const Toggle = defineComponent(function Toggle() {
|
|
548
|
-
const selected = this.signal(
|
|
549
|
-
const
|
|
550
|
-
|
|
772
|
+
const selected = this.signal(false);
|
|
773
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
774
|
+
element.dataset.attached = "true";
|
|
551
775
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
target.dataset.mounted = "true";
|
|
776
|
+
const visible = this.handler("visible", function ({ element }) {
|
|
777
|
+
element.dataset.visible = "true";
|
|
555
778
|
});
|
|
556
779
|
|
|
557
780
|
return html`
|
|
558
781
|
<button
|
|
559
782
|
type="button"
|
|
560
|
-
on:
|
|
561
|
-
|
|
562
|
-
|
|
783
|
+
on:attach="${attach}"
|
|
784
|
+
on:visible="${visible}"
|
|
785
|
+
on:click="${this.handler(function () {
|
|
786
|
+
selected.update((value) => !value);
|
|
787
|
+
})}"
|
|
788
|
+
class:selected="${selected}"
|
|
789
|
+
signal:class="${["toggle", { active: selected }]}"
|
|
790
|
+
signal:attr:aria-pressed="${selected}"
|
|
563
791
|
>
|
|
564
792
|
Toggle
|
|
565
793
|
</button>
|
|
@@ -576,17 +804,46 @@ Component helpers:
|
|
|
576
804
|
|
|
577
805
|
| Helper | Behavior |
|
|
578
806
|
| --- | --- |
|
|
579
|
-
| `this.signal(name, initial)` | Scoped get-or-create signal |
|
|
807
|
+
| `this.signal(name, initial)` | Scoped named get-or-create signal |
|
|
808
|
+
| `this.signal(initial)` | Generated scoped local signal |
|
|
580
809
|
| `this.computed(name, fn)` | Scoped computed signal |
|
|
581
810
|
| `this.asyncSignal(name, fn)` | Scoped async signal |
|
|
582
811
|
| `this.effect(fn)` | Scoped effect with cleanup |
|
|
583
|
-
| `this.handler(name, fn)` | Scoped handler registry entry |
|
|
812
|
+
| `this.handler(name, fn)` | Scoped named handler registry entry |
|
|
813
|
+
| `this.handler(fn)` | Generated scoped handler registry entry |
|
|
584
814
|
| `this.render(Component, props)` | Child fragment rendering |
|
|
585
|
-
| `this.
|
|
586
|
-
| `this.
|
|
815
|
+
| `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
|
|
816
|
+
| `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
|
|
817
|
+
| `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
|
|
818
|
+
|
|
819
|
+
Put component lifecycle on the component root element when there is one:
|
|
820
|
+
|
|
821
|
+
```js
|
|
822
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
823
|
+
element.dataset.attached = "true";
|
|
824
|
+
});
|
|
825
|
+
const visible = this.handler("visible", function ({ element }) {
|
|
826
|
+
element.dataset.visible = "true";
|
|
827
|
+
});
|
|
587
828
|
|
|
588
|
-
|
|
589
|
-
|
|
829
|
+
return html`<article on:attach="${attach}" on:visible="${visible}">...</article>`;
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
If a component returns text or multiple root nodes, use the scoped fallback:
|
|
833
|
+
|
|
834
|
+
```js
|
|
835
|
+
this.on("attach", (target) => {
|
|
836
|
+
target.dataset.attached = "true";
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
this.on("destroy", () => {
|
|
840
|
+
// Clean up fragment-scoped resources.
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
`on:visible` is defined as a component lifecycle pseudo-event. It runs once when
|
|
845
|
+
the component root first becomes visible. Lifecycle events do not drive
|
|
846
|
+
component rerenders.
|
|
590
847
|
|
|
591
848
|
## Streaming
|
|
592
849
|
|
|
@@ -597,7 +854,7 @@ loader.swap(
|
|
|
597
854
|
"product",
|
|
598
855
|
`
|
|
599
856
|
<article>
|
|
600
|
-
<h1
|
|
857
|
+
<h1 signal:text="product.title"></h1>
|
|
601
858
|
<button type="button" on:click="selectProduct">Select</button>
|
|
602
859
|
</article>
|
|
603
860
|
`
|
|
@@ -653,3 +910,22 @@ then runs release doctor.
|
|
|
653
910
|
The core runtime is intentionally small. Bundling, lazy chunk manifests, JSX
|
|
654
911
|
lowering, TSRX lowering, server resource compilation, and higher-level
|
|
655
912
|
resumability metadata are deferred to later layers.
|
|
913
|
+
|
|
914
|
+
## Async And htmx
|
|
915
|
+
|
|
916
|
+
Async and htmx are both HTML-first and avoid a virtual DOM, but they optimize
|
|
917
|
+
for different boundaries.
|
|
918
|
+
|
|
919
|
+
| Area | htmx | Async |
|
|
920
|
+
| --- | --- | --- |
|
|
921
|
+
| Primary model | HTML attributes issue HTTP requests and swap server responses. | HTML attributes bind signals, command events, server calls, and route boundaries. |
|
|
922
|
+
| State | Server-owned hypermedia state; browser state is intentionally minimal. | Browser signal registry plus server signal patches and cache snapshots. |
|
|
923
|
+
| Server interaction | DOM attributes describe HTTP verbs, targets, and swaps. | `server.*(...)` commands call registered server functions and apply returned effects. |
|
|
924
|
+
| Routing | Usually server navigation or htmx-boosted navigation. | CSR, SPA, SSR, SSR-SPA, and MPA router modes built around partial boundaries. |
|
|
925
|
+
| Components | Server-rendered HTML fragments. | Scoped fragment functions today; higher layers can compile JSX/TSRX later. |
|
|
926
|
+
| Build story | No build by default. | Layer 1 is no-build/CDN; higher layers can add build or compiler steps. |
|
|
927
|
+
|
|
928
|
+
Use htmx when the server should own most interaction through hypermedia and
|
|
929
|
+
HTTP swaps. Use Async when you want an HTML-first runtime that also has local
|
|
930
|
+
signals, async resources, registered browser/server handlers, route partials,
|
|
931
|
+
and a path to higher compiler layers without changing the Layer 1 protocol.
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<title>AsyncLoader Cache</title>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<main
|
|
9
|
+
<main async:container>
|
|
10
10
|
<button type="button" on:click="cacheDemo.loadProduct">Load product</button>
|
|
11
|
-
<p>Product: <strong
|
|
12
|
-
<p>Server calls: <strong
|
|
11
|
+
<p>Product: <strong signal:text="cacheDemo.title"></strong></p>
|
|
12
|
+
<p>Server calls: <strong signal:text="cacheDemo.calls"></strong></p>
|
|
13
13
|
</main>
|
|
14
14
|
<script type="module" src="./main.js"></script>
|
|
15
15
|
</body>
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { AsyncLoader, defineComponent, html } from "../../src/index.js";
|
|
2
2
|
|
|
3
3
|
const Toggle = defineComponent(function Toggle() {
|
|
4
|
-
const selected = this.signal(
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
this.onMount(() => {
|
|
10
|
-
document.body.dataset.toggleMounted = "true";
|
|
4
|
+
const selected = this.signal(false);
|
|
5
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
6
|
+
element.dataset.attached = "true";
|
|
11
7
|
});
|
|
12
8
|
|
|
13
9
|
return html`
|
|
14
10
|
<button
|
|
15
11
|
type="button"
|
|
16
|
-
on:
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
on:attach="${attach}"
|
|
13
|
+
on:click="${this.handler(function () {
|
|
14
|
+
selected.update((value) => !value);
|
|
15
|
+
})}"
|
|
16
|
+
class:selected="${selected}"
|
|
17
|
+
signal:class="${["toggle", { active: selected }]}"
|
|
18
|
+
signal:attr:aria-pressed="${selected}"
|
|
19
19
|
>
|
|
20
20
|
Toggle
|
|
21
21
|
</button>
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>AsyncLoader Counter</title>
|
|
6
6
|
</head>
|
|
7
|
-
<body
|
|
7
|
+
<body async:container>
|
|
8
8
|
<main>
|
|
9
|
-
<p>Count: <strong
|
|
9
|
+
<p>Count: <strong signal:text="counter.count"></strong></p>
|
|
10
10
|
<button type="button" on:click="counter.decrement">-</button>
|
|
11
11
|
<button type="button" on:click="counter.increment">+</button>
|
|
12
12
|
</main>
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<title>AsyncLoader Partials</title>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<main
|
|
9
|
+
<main async:container>
|
|
10
10
|
<button type="button" on:click="partialsDemo.loadProduct">Load product</button>
|
|
11
|
-
<section
|
|
11
|
+
<section async:boundary="product"></section>
|
|
12
12
|
</main>
|
|
13
13
|
<script type="module" src="./main.js"></script>
|
|
14
14
|
</body>
|
|
@@ -4,26 +4,26 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>AsyncLoader Product</title>
|
|
6
6
|
</head>
|
|
7
|
-
<body
|
|
7
|
+
<body async:container>
|
|
8
8
|
<main>
|
|
9
9
|
<label>
|
|
10
10
|
Product
|
|
11
|
-
<select
|
|
11
|
+
<select signal:value="productId">
|
|
12
12
|
<option value="sku-1">Mechanical Keyboard</option>
|
|
13
13
|
<option value="sku-2">Studio Headphones</option>
|
|
14
14
|
</select>
|
|
15
15
|
</label>
|
|
16
16
|
|
|
17
|
-
<section
|
|
18
|
-
<template
|
|
17
|
+
<section async:boundary="product">
|
|
18
|
+
<template async:loading="product">
|
|
19
19
|
<p>Loading product...</p>
|
|
20
20
|
</template>
|
|
21
|
-
<template
|
|
22
|
-
<h1
|
|
23
|
-
<p
|
|
21
|
+
<template async:ready="product">
|
|
22
|
+
<h1 signal:text="product.title"></h1>
|
|
23
|
+
<p signal:text="product.description"></p>
|
|
24
24
|
</template>
|
|
25
|
-
<template
|
|
26
|
-
<p
|
|
25
|
+
<template async:error="product">
|
|
26
|
+
<p signal:text="product.$error.message"></p>
|
|
27
27
|
</template>
|
|
28
28
|
</section>
|
|
29
29
|
</main>
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<title>AsyncLoader CSR Router</title>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<main
|
|
9
|
+
<main async:container>
|
|
10
10
|
<nav>
|
|
11
11
|
<a href="/">Home</a>
|
|
12
12
|
<a href="/products/sku-1">Product</a>
|
|
13
13
|
</nav>
|
|
14
|
-
<section
|
|
14
|
+
<section async:boundary="route"></section>
|
|
15
15
|
</main>
|
|
16
16
|
<script type="module" src="./main.js"></script>
|
|
17
17
|
</body>
|