@async/framework 0.3.0 → 0.5.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 +28 -0
- package/README.md +254 -34
- package/examples/components/main.js +10 -10
- package/examples/ssr/main.js +1 -1
- package/examples/streaming/main.js +1 -1
- package/framework.js +4133 -0
- package/package.json +9 -5
- package/src/async-signal.js +12 -1
- package/src/attributes.js +2 -0
- package/src/cache.js +5 -0
- package/src/component.js +149 -17
- package/src/handlers.js +12 -4
- package/src/html.js +99 -6
- package/src/loader.js +311 -18
- package/src/partials.js +20 -7
- package/src/registry-store.js +4 -0
- package/src/router.js +9 -0
- package/src/server.js +26 -6
- package/src/signals.js +16 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-06-17
|
|
4
|
+
|
|
5
|
+
- Added `this.suspense(signalRef, views)` for component-owned async boundary
|
|
6
|
+
templates without adding a wrapper, rerender loop, hydration, or promise
|
|
7
|
+
throwing.
|
|
8
|
+
- Added `signal:prop:*` property bindings and tests for inline signal refs in
|
|
9
|
+
`signal:text`, `signal:attr:*`, and `signal:prop:*`.
|
|
10
|
+
- Added explicit `unregister(id)` APIs to runtime registries and component
|
|
11
|
+
cleanup for scoped signals, async signals, computed signals, and handlers.
|
|
12
|
+
- Added boundary swap cleanup for mounted component fragments and old DOM
|
|
13
|
+
bindings.
|
|
14
|
+
- Added server-call normalization for async signals, including returned signal
|
|
15
|
+
effects, proxy abort propagation, and stable server error messages.
|
|
16
|
+
- Added `prevent` as a command-event alias for `preventDefault`.
|
|
17
|
+
|
|
18
|
+
## 0.4.0 - 2026-06-17
|
|
19
|
+
|
|
20
|
+
- Added a generated root `framework.js` ESM bundle for UNPKG browser imports.
|
|
21
|
+
- Expanded the README with Async layer definitions and an htmx comparison.
|
|
22
|
+
- Added `on:attach` as the canonical component attach lifecycle pseudo-event
|
|
23
|
+
with `on:mount` kept as a compatibility alias.
|
|
24
|
+
- Added top-level `class:*` bindings, including aggregate `class:`
|
|
25
|
+
string/object/array class sets.
|
|
26
|
+
- Added inline `html` template bindings for signal refs, class arrays/objects,
|
|
27
|
+
`value="${signalRef}"`, and generated component handlers via
|
|
28
|
+
`this.handler(fn)`.
|
|
29
|
+
- Added generated component-local signals via `this.signal(initial)`.
|
|
30
|
+
|
|
3
31
|
## 0.3.0 - 2026-06-17
|
|
4
32
|
|
|
5
33
|
- Added a shared registry store behind `Async`, app runtimes, and concrete
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
|
@@ -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
|
|
|
@@ -71,9 +90,9 @@ and package lifecycle tooling. Browser consumers import ESM directly.
|
|
|
71
90
|
|
|
72
91
|
## CDN
|
|
73
92
|
|
|
74
|
-
The package
|
|
75
|
-
step. Use `@latest` for quick prototypes, and pin an exact
|
|
76
|
-
production:
|
|
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:
|
|
77
96
|
|
|
78
97
|
```html
|
|
79
98
|
<main async:container>
|
|
@@ -85,7 +104,7 @@ production:
|
|
|
85
104
|
import {
|
|
86
105
|
Async,
|
|
87
106
|
createSignal
|
|
88
|
-
} from "https://unpkg.com/@async/framework@latest";
|
|
107
|
+
} from "https://unpkg.com/@async/framework@latest/framework.js";
|
|
89
108
|
|
|
90
109
|
Async.use({
|
|
91
110
|
signal: {
|
|
@@ -108,7 +127,7 @@ You can also use an import map so app code imports `@async/framework` by name:
|
|
|
108
127
|
<script type="importmap">
|
|
109
128
|
{
|
|
110
129
|
"imports": {
|
|
111
|
-
"@async/framework": "https://unpkg.com/@async/framework@latest"
|
|
130
|
+
"@async/framework": "https://unpkg.com/@async/framework@latest/framework.js"
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
</script>
|
|
@@ -234,6 +253,7 @@ Naming rules:
|
|
|
234
253
|
| `create*` | Runtime instance or mutable runtime primitive |
|
|
235
254
|
| `Async.use(...)` | App-level declaration registration |
|
|
236
255
|
| `registry.register(...)` | Low-level registration on a concrete runtime registry |
|
|
256
|
+
| `registry.unregister(...)` | Low-level removal from a concrete runtime registry |
|
|
237
257
|
|
|
238
258
|
Singular registry keys are canonical: `signal`, `handler`, `server`,
|
|
239
259
|
`partial`, `route`, `component`, and nested `cache.browser` / `cache.server`.
|
|
@@ -295,6 +315,7 @@ signals.set("count", 1);
|
|
|
295
315
|
signals.update("count", (count) => count + 1);
|
|
296
316
|
signals.subscribe("count", (count) => console.log(count));
|
|
297
317
|
signals.ref("count").value;
|
|
318
|
+
signals.unregister("count");
|
|
298
319
|
```
|
|
299
320
|
|
|
300
321
|
Initializer maps are supported:
|
|
@@ -365,10 +386,14 @@ AsyncLoader scans regular HTML attributes:
|
|
|
365
386
|
| `on:click="selectProduct"` | Delegated command event |
|
|
366
387
|
| `on:submit="preventDefault; save"` | Sequential command chain |
|
|
367
388
|
| `on:click="server.cart.add(productId)"` | Server command with signal args |
|
|
389
|
+
| `on:attach="setup"` | Component root attach lifecycle pseudo-event |
|
|
390
|
+
| `on:visible="trackView"` | Component root visible lifecycle pseudo-event |
|
|
368
391
|
| `signal:text="product.title"` | Text binding |
|
|
369
392
|
| `signal:value="productId"` | Form value binding with writeback |
|
|
370
393
|
| `signal:attr:disabled="product.$loading"` | Attribute binding |
|
|
371
|
-
| `signal:
|
|
394
|
+
| `signal:prop:checked="selected"` | DOM property binding |
|
|
395
|
+
| `class:selected="selected"` | Class toggle from a signal path |
|
|
396
|
+
| `signal:class="buttonClasses"` | Class set from a signal value: string, object, or array |
|
|
372
397
|
| `async:boundary="product"` | Async or streamed replacement boundary |
|
|
373
398
|
| `async:loading="product"` | Boundary loading template |
|
|
374
399
|
| `async:ready="product"` | Boundary ready template |
|
|
@@ -396,19 +421,110 @@ Async.start({
|
|
|
396
421
|
root: document,
|
|
397
422
|
attributes: {
|
|
398
423
|
async: "data-async-",
|
|
424
|
+
class: "data-class-",
|
|
399
425
|
signal: "data-signal-",
|
|
400
426
|
on: "data-on-"
|
|
401
427
|
}
|
|
402
428
|
});
|
|
403
429
|
```
|
|
404
430
|
|
|
405
|
-
That maps to `data-async-container`, `data-on-click="save"`,
|
|
406
|
-
`data-signal-text="product.title"`.
|
|
431
|
+
That maps to `data-async-container`, `data-on-click="save"`,
|
|
432
|
+
`data-signal-text="product.title"`, and `data-class-selected="selected"`.
|
|
433
|
+
|
|
434
|
+
Inside `html` templates, signal refs can be passed directly to binding
|
|
435
|
+
attributes:
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
const title = this.signal("Keyboard");
|
|
439
|
+
const disabled = this.signal(false);
|
|
440
|
+
const checked = this.signal(true);
|
|
441
|
+
|
|
442
|
+
return html`
|
|
443
|
+
<h1 signal:text="${title}"></h1>
|
|
444
|
+
<button signal:attr:disabled="${disabled}">Save</button>
|
|
445
|
+
<input type="checkbox" signal:prop:checked="${checked}">
|
|
446
|
+
`;
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Use `signal:value` for form value binding with writeback. Use `signal:prop:*`
|
|
450
|
+
when you only need one-way DOM property updates.
|
|
451
|
+
|
|
452
|
+
Named class toggles use their own top-level namespace:
|
|
453
|
+
|
|
454
|
+
```html
|
|
455
|
+
<button
|
|
456
|
+
class="button"
|
|
457
|
+
class:selected="selected"
|
|
458
|
+
>
|
|
459
|
+
Add
|
|
460
|
+
</button>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Aggregate class binding uses `signal:class`. It reads the current signal value
|
|
464
|
+
and accepts strings, objects, and arrays:
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
Async.use({
|
|
468
|
+
signal: {
|
|
469
|
+
buttonClasses: createSignal([
|
|
470
|
+
"button-primary",
|
|
471
|
+
{ selected: true, disabled: false },
|
|
472
|
+
["compact"]
|
|
473
|
+
])
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
```html
|
|
479
|
+
<button signal:class="buttonClasses">Add</button>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Inside `html` templates, `signal:class` can also receive objects or arrays
|
|
483
|
+
directly. Signal refs inside the object or array are tracked:
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
const selected = this.signal("selected", false);
|
|
487
|
+
const tone = this.signal("tone", "primary");
|
|
488
|
+
|
|
489
|
+
return html`
|
|
490
|
+
<article signal:class="${["card", tone, { selected }]}"}>
|
|
491
|
+
...
|
|
492
|
+
</article>
|
|
493
|
+
`;
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
For component-local state that does not need a stable public id, omit the name.
|
|
497
|
+
The signal is still registered under the component scope:
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
const selected = this.signal(false);
|
|
501
|
+
const tone = this.signal("primary");
|
|
502
|
+
|
|
503
|
+
return html`
|
|
504
|
+
<article signal:class="${["card", selected, tone]}">
|
|
505
|
+
...
|
|
506
|
+
</article>
|
|
507
|
+
`;
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`value="${signalRef}"` in an `html` template is equivalent to adding
|
|
511
|
+
`signal:value` for that signal. It writes back on input/change:
|
|
512
|
+
|
|
513
|
+
```js
|
|
514
|
+
const productId = this.signal("productId", "sku-1");
|
|
515
|
+
|
|
516
|
+
return html`<input value="${productId}">`;
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
`signal:class:selected="selected"` remains supported as a compatibility alias,
|
|
520
|
+
but new examples should use `class:selected`. The parser-safe top-level
|
|
521
|
+
aggregate form `class:="buttonClasses"` also remains supported.
|
|
407
522
|
|
|
408
523
|
### Command Events
|
|
409
524
|
|
|
410
|
-
`on:*` works with any native DOM event name. `on:
|
|
411
|
-
reserved pseudo-events with cleanup support.
|
|
525
|
+
`on:*` works with any native DOM event name. `on:attach` and `on:visible` are
|
|
526
|
+
reserved component lifecycle pseudo-events with cleanup support. `on:mount`
|
|
527
|
+
remains as a compatibility alias for `on:attach`.
|
|
412
528
|
|
|
413
529
|
Command chains use semicolons and are awaited sequentially:
|
|
414
530
|
|
|
@@ -423,6 +539,7 @@ Plain commands resolve through the handler registry. Built-ins are registered by
|
|
|
423
539
|
default:
|
|
424
540
|
|
|
425
541
|
```txt
|
|
542
|
+
prevent
|
|
426
543
|
preventDefault
|
|
427
544
|
stopPropagation
|
|
428
545
|
stopImmediatePropagation
|
|
@@ -489,6 +606,12 @@ await server.cart.add("sku-1", 2);
|
|
|
489
606
|
|
|
490
607
|
Server responses can include `value`, `signals`, `boundary`, `html`, `redirect`,
|
|
491
608
|
or `error`. Signal patches are applied before boundary swaps and redirects.
|
|
609
|
+
Namespace calls such as `server.cart.add(...)` return the unwrapped `value`.
|
|
610
|
+
|
|
611
|
+
When an async signal calls a server namespace function, the framework passes the
|
|
612
|
+
active abort signal through proxy calls. Returned server effects such as
|
|
613
|
+
`signals`, `cache.browser`, `boundary/html`, and `redirect` are applied before
|
|
614
|
+
the async signal stores the unwrapped `value`.
|
|
492
615
|
|
|
493
616
|
### Router And Partials
|
|
494
617
|
|
|
@@ -674,21 +797,25 @@ type and no rerender loop.
|
|
|
674
797
|
|
|
675
798
|
```js
|
|
676
799
|
const Toggle = defineComponent(function Toggle() {
|
|
677
|
-
const selected = this.signal(
|
|
678
|
-
const
|
|
679
|
-
|
|
800
|
+
const selected = this.signal(false);
|
|
801
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
802
|
+
element.dataset.attached = "true";
|
|
680
803
|
});
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
target.dataset.mounted = "true";
|
|
804
|
+
const visible = this.handler("visible", function ({ element }) {
|
|
805
|
+
element.dataset.visible = "true";
|
|
684
806
|
});
|
|
685
807
|
|
|
686
808
|
return html`
|
|
687
809
|
<button
|
|
688
810
|
type="button"
|
|
689
|
-
on:
|
|
690
|
-
|
|
691
|
-
|
|
811
|
+
on:attach="${attach}"
|
|
812
|
+
on:visible="${visible}"
|
|
813
|
+
on:click="${this.handler(function () {
|
|
814
|
+
selected.update((value) => !value);
|
|
815
|
+
})}"
|
|
816
|
+
class:selected="${selected}"
|
|
817
|
+
signal:class="${["toggle", { active: selected }]}"
|
|
818
|
+
signal:attr:aria-pressed="${selected}"
|
|
692
819
|
>
|
|
693
820
|
Toggle
|
|
694
821
|
</button>
|
|
@@ -705,17 +832,91 @@ Component helpers:
|
|
|
705
832
|
|
|
706
833
|
| Helper | Behavior |
|
|
707
834
|
| --- | --- |
|
|
708
|
-
| `this.signal(name, initial)` | Scoped get-or-create signal |
|
|
835
|
+
| `this.signal(name, initial)` | Scoped named get-or-create signal |
|
|
836
|
+
| `this.signal(initial)` | Generated scoped local signal |
|
|
709
837
|
| `this.computed(name, fn)` | Scoped computed signal |
|
|
710
838
|
| `this.asyncSignal(name, fn)` | Scoped async signal |
|
|
711
839
|
| `this.effect(fn)` | Scoped effect with cleanup |
|
|
712
|
-
| `this.handler(name, fn)` | Scoped handler registry entry |
|
|
840
|
+
| `this.handler(name, fn)` | Scoped named handler registry entry |
|
|
841
|
+
| `this.handler(fn)` | Generated scoped handler registry entry |
|
|
713
842
|
| `this.render(Component, props)` | Child fragment rendering |
|
|
714
|
-
| `this.
|
|
715
|
-
| `this.
|
|
843
|
+
| `this.suspense(signalRef, views)` | Async boundary template helper |
|
|
844
|
+
| `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
|
|
845
|
+
| `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
|
|
846
|
+
| `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
|
|
847
|
+
|
|
848
|
+
`this.suspense(...)` is sugar for AsyncLoader boundaries:
|
|
849
|
+
`asyncSignal + async:boundary + async:* templates`. It emits only templates. The
|
|
850
|
+
caller owns the boundary element, and the loader chooses the loading, ready, or
|
|
851
|
+
error template from the async signal status.
|
|
852
|
+
|
|
853
|
+
```js
|
|
854
|
+
const Product = defineComponent(function Product() {
|
|
855
|
+
const product = this.asyncSignal("product", async function () {
|
|
856
|
+
return this.server.products.get("sku-1");
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
return html`
|
|
860
|
+
<article async:boundary="${product.id}">
|
|
861
|
+
${this.suspense(product, {
|
|
862
|
+
loading() {
|
|
863
|
+
return html`<p>Loading...</p>`;
|
|
864
|
+
},
|
|
865
|
+
ready(product) {
|
|
866
|
+
return html`<h1 signal:text="${product.id}.title"></h1>`;
|
|
867
|
+
},
|
|
868
|
+
error(product) {
|
|
869
|
+
return html`<p signal:text="${product.id}.$error.message"></p>`;
|
|
870
|
+
}
|
|
871
|
+
})}
|
|
872
|
+
</article>
|
|
873
|
+
`;
|
|
874
|
+
});
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
The shorthand form treats the callback as the ready template:
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
this.suspense(product, (product) => html`
|
|
881
|
+
<h1 signal:text="${product.id}.title"></h1>
|
|
882
|
+
`);
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
`this.suspense(...)` is not React Suspense. It does not throw promises,
|
|
886
|
+
hydrate, diff, rerender a component tree, or emit a wrapper element.
|
|
716
887
|
|
|
717
|
-
|
|
718
|
-
|
|
888
|
+
Component-scoped signals and handlers are unregistered when the mounted
|
|
889
|
+
fragment is destroyed. `loader.swap(...)` cleans up old DOM bindings and mounted
|
|
890
|
+
component fragments under the swapped boundary before inserting the new HTML.
|
|
891
|
+
|
|
892
|
+
Put component lifecycle on the component root element when there is one:
|
|
893
|
+
|
|
894
|
+
```js
|
|
895
|
+
const attach = this.handler("attach", function ({ element }) {
|
|
896
|
+
element.dataset.attached = "true";
|
|
897
|
+
});
|
|
898
|
+
const visible = this.handler("visible", function ({ element }) {
|
|
899
|
+
element.dataset.visible = "true";
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
return html`<article on:attach="${attach}" on:visible="${visible}">...</article>`;
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
If a component returns text or multiple root nodes, use the scoped fallback:
|
|
906
|
+
|
|
907
|
+
```js
|
|
908
|
+
this.on("attach", (target) => {
|
|
909
|
+
target.dataset.attached = "true";
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
this.on("destroy", () => {
|
|
913
|
+
// Clean up fragment-scoped resources.
|
|
914
|
+
});
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
`on:visible` is defined as a component lifecycle pseudo-event. It runs once when
|
|
918
|
+
the component root first becomes visible. Lifecycle events do not drive
|
|
919
|
+
component rerenders.
|
|
719
920
|
|
|
720
921
|
## Streaming
|
|
721
922
|
|
|
@@ -782,3 +983,22 @@ then runs release doctor.
|
|
|
782
983
|
The core runtime is intentionally small. Bundling, lazy chunk manifests, JSX
|
|
783
984
|
lowering, TSRX lowering, server resource compilation, and higher-level
|
|
784
985
|
resumability metadata are deferred to later layers.
|
|
986
|
+
|
|
987
|
+
## Async And htmx
|
|
988
|
+
|
|
989
|
+
Async and htmx are both HTML-first and avoid a virtual DOM, but they optimize
|
|
990
|
+
for different boundaries.
|
|
991
|
+
|
|
992
|
+
| Area | htmx | Async |
|
|
993
|
+
| --- | --- | --- |
|
|
994
|
+
| Primary model | HTML attributes issue HTTP requests and swap server responses. | HTML attributes bind signals, command events, server calls, and route boundaries. |
|
|
995
|
+
| State | Server-owned hypermedia state; browser state is intentionally minimal. | Browser signal registry plus server signal patches and cache snapshots. |
|
|
996
|
+
| Server interaction | DOM attributes describe HTTP verbs, targets, and swaps. | `server.*(...)` commands call registered server functions and apply returned effects. |
|
|
997
|
+
| Routing | Usually server navigation or htmx-boosted navigation. | CSR, SPA, SSR, SSR-SPA, and MPA router modes built around partial boundaries. |
|
|
998
|
+
| Components | Server-rendered HTML fragments. | Scoped fragment functions today; higher layers can compile JSX/TSRX later. |
|
|
999
|
+
| Build story | No build by default. | Layer 1 is no-build/CDN; higher layers can add build or compiler steps. |
|
|
1000
|
+
|
|
1001
|
+
Use htmx when the server should own most interaction through hypermedia and
|
|
1002
|
+
HTTP swaps. Use Async when you want an HTML-first runtime that also has local
|
|
1003
|
+
signals, async resources, registered browser/server handlers, route partials,
|
|
1004
|
+
and a path to higher compiler layers without changing the Layer 1 protocol.
|
|
@@ -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>
|
package/examples/ssr/main.js
CHANGED
|
@@ -56,7 +56,7 @@ serverApp.use({
|
|
|
56
56
|
<article>
|
|
57
57
|
<h1>${product.title}</h1>
|
|
58
58
|
<p>${product.id}</p>
|
|
59
|
-
<button type="button" on:click="ssrDemo.selectProduct"
|
|
59
|
+
<button type="button" on:click="ssrDemo.selectProduct" class:selected="ssrDemo.selected">
|
|
60
60
|
Select
|
|
61
61
|
</button>
|
|
62
62
|
</article>
|
|
@@ -14,7 +14,7 @@ Async.use({
|
|
|
14
14
|
`
|
|
15
15
|
<article>
|
|
16
16
|
<h1 signal:text="streamingDemo.title"></h1>
|
|
17
|
-
<button type="button" on:click="streamingDemo.select"
|
|
17
|
+
<button type="button" on:click="streamingDemo.select" class:selected="streamingDemo.selected">
|
|
18
18
|
Select
|
|
19
19
|
</button>
|
|
20
20
|
</article>
|