@async/framework 0.4.0 → 0.6.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 +32 -0
- package/README.md +124 -7
- package/examples/cache/index.html +1 -1
- package/examples/components/index.html +1 -1
- package/examples/components/main.js +2 -2
- package/examples/counter/index.html +1 -1
- package/examples/partials/index.html +1 -1
- package/examples/product/index.html +1 -1
- package/examples/product/main.js +2 -2
- package/examples/router/index.html +1 -1
- package/examples/server-call/index.html +1 -1
- package/examples/ssr/index.html +1 -1
- package/examples/streaming/index.html +1 -1
- package/framework.d.ts +569 -0
- package/framework.js +325 -101
- package/framework.min.js +3648 -0
- package/framework.ts +3 -0
- package/framework.umd.js +4158 -0
- package/framework.umd.min.js +3671 -0
- package/package.json +34 -5
- package/src/app.js +2 -2
- package/src/async-signal.js +12 -1
- package/src/cache.js +5 -0
- package/src/component.js +80 -16
- package/src/handlers.js +12 -4
- package/src/index.js +1 -1
- package/src/loader.js +93 -9
- package/src/partials.js +5 -0
- package/src/registry-store.js +4 -0
- package/src/router.js +11 -2
- package/src/server.js +26 -6
- package/src/signals.js +16 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "No-build
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "No-build Loader app runtime with signals, command events, server calls, route partials, cache split, SSR activation, and streaming boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.1.0",
|
|
7
7
|
"engines": {
|
|
@@ -28,21 +28,49 @@
|
|
|
28
28
|
"web-framework"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
|
-
"
|
|
31
|
+
"types": "./framework.d.ts",
|
|
32
|
+
"unpkg": "./framework.umd.min.js",
|
|
33
|
+
"jsdelivr": "./framework.umd.min.js",
|
|
32
34
|
"exports": {
|
|
33
35
|
".": {
|
|
34
|
-
"
|
|
36
|
+
"types": "./framework.d.ts",
|
|
37
|
+
"unpkg": "./framework.umd.min.js",
|
|
35
38
|
"browser": "./framework.js",
|
|
36
39
|
"import": "./src/index.js",
|
|
37
40
|
"default": "./src/index.js"
|
|
38
41
|
},
|
|
39
|
-
"./framework.js":
|
|
42
|
+
"./framework.js": {
|
|
43
|
+
"types": "./framework.d.ts",
|
|
44
|
+
"default": "./framework.js"
|
|
45
|
+
},
|
|
46
|
+
"./framework.min.js": {
|
|
47
|
+
"types": "./framework.d.ts",
|
|
48
|
+
"default": "./framework.min.js"
|
|
49
|
+
},
|
|
50
|
+
"./framework.umd.js": {
|
|
51
|
+
"types": "./framework.d.ts",
|
|
52
|
+
"default": "./framework.umd.js"
|
|
53
|
+
},
|
|
54
|
+
"./framework.umd.min.js": {
|
|
55
|
+
"types": "./framework.d.ts",
|
|
56
|
+
"default": "./framework.umd.min.js"
|
|
57
|
+
},
|
|
58
|
+
"./framework.ts": {
|
|
59
|
+
"types": "./framework.d.ts",
|
|
60
|
+
"default": "./framework.ts"
|
|
61
|
+
},
|
|
62
|
+
"./framework.d.ts": "./framework.d.ts",
|
|
40
63
|
"./package.json": "./package.json"
|
|
41
64
|
},
|
|
42
65
|
"files": [
|
|
43
66
|
"CHANGELOG.md",
|
|
44
67
|
"README.md",
|
|
68
|
+
"framework.d.ts",
|
|
45
69
|
"framework.js",
|
|
70
|
+
"framework.min.js",
|
|
71
|
+
"framework.umd.js",
|
|
72
|
+
"framework.umd.min.js",
|
|
73
|
+
"framework.ts",
|
|
46
74
|
"src",
|
|
47
75
|
"examples",
|
|
48
76
|
"LICENSE",
|
|
@@ -68,6 +96,7 @@
|
|
|
68
96
|
"pipeline:sync:generate": "async-pipeline sync generate",
|
|
69
97
|
"pipeline:task:docs.site": "async-pipeline run-task docs.site",
|
|
70
98
|
"pipeline:verify": "async-pipeline run verify",
|
|
99
|
+
"registry:lint": "node scripts/registry-lint.js",
|
|
71
100
|
"release:check": "pnpm run pipeline:verify -- --force && pnpm run pipeline:pages -- --force && pnpm run pipeline:sync:check && pnpm run pipeline:github:check",
|
|
72
101
|
"test": "node --test tests/*.test.js"
|
|
73
102
|
},
|
package/src/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCacheRegistry } from "./cache.js";
|
|
2
2
|
import { createComponentRegistry } from "./component.js";
|
|
3
3
|
import { createHandlerRegistry } from "./handlers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { Loader } from "./loader.js";
|
|
5
5
|
import { createPartialRegistry } from "./partials.js";
|
|
6
6
|
import { createRouteRegistry, createRouter } from "./router.js";
|
|
7
7
|
import { createServerRegistry } from "./server.js";
|
|
@@ -101,7 +101,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
101
101
|
started = true;
|
|
102
102
|
|
|
103
103
|
if (target !== "server") {
|
|
104
|
-
loader = loader ??
|
|
104
|
+
loader = loader ?? Loader({
|
|
105
105
|
root: options.root,
|
|
106
106
|
signals,
|
|
107
107
|
handlers,
|
package/src/async-signal.js
CHANGED
|
@@ -82,7 +82,18 @@ export function asyncSignal(id, fn) {
|
|
|
82
82
|
signals: registry,
|
|
83
83
|
id: registeredId,
|
|
84
84
|
get server() {
|
|
85
|
-
|
|
85
|
+
const context = registry._context?.() ?? {};
|
|
86
|
+
const server = context.server;
|
|
87
|
+
if (typeof server?._withContext === "function") {
|
|
88
|
+
return server._withContext({
|
|
89
|
+
signals: registry,
|
|
90
|
+
router: context.router,
|
|
91
|
+
loader: context.loader,
|
|
92
|
+
cache: context.cache,
|
|
93
|
+
abort: activeAbort
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return server;
|
|
86
97
|
},
|
|
87
98
|
get router() {
|
|
88
99
|
return registry._context?.().router;
|
package/src/cache.js
CHANGED
|
@@ -34,6 +34,11 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
|
|
|
34
34
|
return registryApi;
|
|
35
35
|
},
|
|
36
36
|
|
|
37
|
+
unregister(id) {
|
|
38
|
+
assertId(id);
|
|
39
|
+
return definitions.delete(id);
|
|
40
|
+
},
|
|
41
|
+
|
|
37
42
|
resolve(id) {
|
|
38
43
|
assertId(id);
|
|
39
44
|
return definitions.get(id);
|
package/src/component.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attributeName } from "./attributes.js";
|
|
2
|
+
import { escapeHtml, rawHtml, renderTemplate } from "./html.js";
|
|
2
3
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
3
4
|
|
|
4
5
|
const componentKind = Symbol.for("@async/framework.component");
|
|
@@ -44,6 +45,13 @@ export function createComponentRegistry(initialMap = {}, options = {}) {
|
|
|
44
45
|
return registry;
|
|
45
46
|
},
|
|
46
47
|
|
|
48
|
+
unregister(id) {
|
|
49
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
50
|
+
throw new TypeError("Component id must be a non-empty string.");
|
|
51
|
+
}
|
|
52
|
+
return entries.delete(id);
|
|
53
|
+
},
|
|
54
|
+
|
|
47
55
|
resolve(id) {
|
|
48
56
|
if (typeof id !== "string" || id.length === 0) {
|
|
49
57
|
throw new TypeError("Component id must be a non-empty string.");
|
|
@@ -75,29 +83,32 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
75
83
|
const visibleHooks = [];
|
|
76
84
|
const destroyHooks = [];
|
|
77
85
|
const bindingIds = [];
|
|
78
|
-
const
|
|
79
|
-
runtime,
|
|
80
|
-
scope,
|
|
81
|
-
cleanups,
|
|
82
|
-
attachHooks,
|
|
83
|
-
visibleHooks,
|
|
84
|
-
destroyHooks
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const output = Component.call(context, props);
|
|
88
|
-
const html = renderTemplate(output, {
|
|
86
|
+
const templateOptions = {
|
|
89
87
|
attributes: runtime.attributes,
|
|
90
88
|
signals: runtime.signals,
|
|
91
89
|
bind(value) {
|
|
92
90
|
const id = runtime.loader?._registerBinding?.(value);
|
|
93
91
|
if (!id) {
|
|
94
|
-
throw new Error("Inline template bindings require
|
|
92
|
+
throw new Error("Inline template bindings require a Loader.");
|
|
95
93
|
}
|
|
96
94
|
bindingIds.push(id);
|
|
97
95
|
return id;
|
|
98
96
|
}
|
|
97
|
+
};
|
|
98
|
+
const renderScopedTemplate = (value) => renderTemplate(value, templateOptions);
|
|
99
|
+
const context = createComponentContext({
|
|
100
|
+
runtime,
|
|
101
|
+
scope,
|
|
102
|
+
cleanups,
|
|
103
|
+
attachHooks,
|
|
104
|
+
visibleHooks,
|
|
105
|
+
destroyHooks,
|
|
106
|
+
renderScopedTemplate
|
|
99
107
|
});
|
|
100
108
|
|
|
109
|
+
const output = Component.call(context, props);
|
|
110
|
+
const html = renderScopedTemplate(output);
|
|
111
|
+
|
|
101
112
|
return {
|
|
102
113
|
html,
|
|
103
114
|
attach(target) {
|
|
@@ -133,7 +144,7 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
133
144
|
};
|
|
134
145
|
}
|
|
135
146
|
|
|
136
|
-
function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks }) {
|
|
147
|
+
function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
|
|
137
148
|
const { signals, handlers, loader, server, router, cache } = runtime;
|
|
138
149
|
const generatedHandlers = new WeakMap();
|
|
139
150
|
let generatedHandlerCounter = 0;
|
|
@@ -149,14 +160,27 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
|
|
|
149
160
|
|
|
150
161
|
signal(name, initial) {
|
|
151
162
|
if (arguments.length === 1) {
|
|
152
|
-
|
|
163
|
+
const id = scoped(scope, `signal.${++generatedSignalCounter}`);
|
|
164
|
+
const ref = signals.ensure(id, name);
|
|
165
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
166
|
+
return ref;
|
|
167
|
+
}
|
|
168
|
+
const id = scoped(scope, name);
|
|
169
|
+
const created = !signals.has(id);
|
|
170
|
+
const ref = signals.ensure(id, initial);
|
|
171
|
+
if (created) {
|
|
172
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
153
173
|
}
|
|
154
|
-
return
|
|
174
|
+
return ref;
|
|
155
175
|
},
|
|
156
176
|
|
|
157
177
|
computed(name, fn) {
|
|
158
178
|
const id = scoped(scope, name);
|
|
179
|
+
const created = !signals.has(id);
|
|
159
180
|
const ref = signals.ensure(id, undefined);
|
|
181
|
+
if (created) {
|
|
182
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
183
|
+
}
|
|
160
184
|
const cleanup = signals.effect(() => {
|
|
161
185
|
signals.set(id, fn.call(context));
|
|
162
186
|
});
|
|
@@ -166,9 +190,13 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
|
|
|
166
190
|
|
|
167
191
|
asyncSignal(name, fn) {
|
|
168
192
|
const id = scoped(scope, name);
|
|
193
|
+
const created = !signals.has(id);
|
|
169
194
|
if (!signals.has(id)) {
|
|
170
195
|
signals.asyncSignal(id, fn);
|
|
171
196
|
}
|
|
197
|
+
if (created) {
|
|
198
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
199
|
+
}
|
|
172
200
|
return signals.ref(id);
|
|
173
201
|
},
|
|
174
202
|
|
|
@@ -202,6 +230,26 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
|
|
|
202
230
|
return rawHtml(child.html);
|
|
203
231
|
},
|
|
204
232
|
|
|
233
|
+
suspense(signalRef, views) {
|
|
234
|
+
const id = signalRef?.id;
|
|
235
|
+
if (!id) {
|
|
236
|
+
throw new TypeError("this.suspense(signalRef, views) requires a signal ref.");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const normalized = normalizeSuspenseViews(views);
|
|
240
|
+
const chunks = [];
|
|
241
|
+
for (const state of ["loading", "ready", "error"]) {
|
|
242
|
+
const view = normalized[state];
|
|
243
|
+
if (!view) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const attr = attributeName(runtime.attributes, "async", state);
|
|
247
|
+
const body = renderScopedTemplate(view.call(context, signalRef));
|
|
248
|
+
chunks.push(`<template ${attr}="${escapeHtml(id)}">${body}</template>`);
|
|
249
|
+
}
|
|
250
|
+
return rawHtml(chunks.join(""));
|
|
251
|
+
},
|
|
252
|
+
|
|
205
253
|
on(eventName, fn) {
|
|
206
254
|
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
207
255
|
throw new TypeError("Component lifecycle event must be a non-empty string.");
|
|
@@ -241,6 +289,7 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
|
|
|
241
289
|
handlers.register(id, function runComponentHandler(handlerContext) {
|
|
242
290
|
return fn.call({ ...context, ...handlerContext }, handlerContext);
|
|
243
291
|
});
|
|
292
|
+
cleanups.push(() => handlers.unregister?.(id));
|
|
244
293
|
return id;
|
|
245
294
|
}
|
|
246
295
|
}
|
|
@@ -252,6 +301,21 @@ function scoped(scope, name) {
|
|
|
252
301
|
return `${scope}.${name}`;
|
|
253
302
|
}
|
|
254
303
|
|
|
304
|
+
function normalizeSuspenseViews(views) {
|
|
305
|
+
const normalized = typeof views === "function" ? { ready: views } : views;
|
|
306
|
+
if (!normalized || typeof normalized !== "object" || Array.isArray(normalized)) {
|
|
307
|
+
throw new TypeError("this.suspense(signalRef, views) requires views to be a function or object.");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const state of ["loading", "ready", "error"]) {
|
|
311
|
+
if (Object.hasOwn(normalized, state) && normalized[state] !== undefined && typeof normalized[state] !== "function") {
|
|
312
|
+
throw new TypeError(`this.suspense(signalRef, views) view "${state}" must be a function.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return normalized;
|
|
317
|
+
}
|
|
318
|
+
|
|
255
319
|
function componentName(Component) {
|
|
256
320
|
return Component.displayName || Component.name || "anonymous";
|
|
257
321
|
}
|
package/src/handlers.js
CHANGED
|
@@ -6,11 +6,10 @@ import {
|
|
|
6
6
|
} from "./server.js";
|
|
7
7
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
8
8
|
|
|
9
|
-
const builtInTokens = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"]);
|
|
9
|
+
const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
|
|
10
10
|
const builtInHandlers = {
|
|
11
|
-
preventDefault
|
|
12
|
-
|
|
13
|
-
},
|
|
11
|
+
prevent: preventDefault,
|
|
12
|
+
preventDefault,
|
|
14
13
|
stopPropagation() {
|
|
15
14
|
this.event?.stopPropagation?.();
|
|
16
15
|
},
|
|
@@ -19,6 +18,10 @@ const builtInHandlers = {
|
|
|
19
18
|
}
|
|
20
19
|
};
|
|
21
20
|
|
|
21
|
+
function preventDefault() {
|
|
22
|
+
this.event?.preventDefault?.();
|
|
23
|
+
}
|
|
24
|
+
|
|
22
25
|
export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
23
26
|
const registryStore = options.registry ?? createRegistryStore();
|
|
24
27
|
const type = options.type ?? "handler";
|
|
@@ -44,6 +47,11 @@ export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
|
44
47
|
return registry;
|
|
45
48
|
},
|
|
46
49
|
|
|
50
|
+
unregister(id) {
|
|
51
|
+
assertId(id);
|
|
52
|
+
return handlers.delete(id);
|
|
53
|
+
},
|
|
54
|
+
|
|
47
55
|
resolve(id) {
|
|
48
56
|
assertId(id);
|
|
49
57
|
return handlers.get(id);
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { component, createComponentRegistry, defineComponent } from "./component
|
|
|
6
6
|
export { delay } from "./delay.js";
|
|
7
7
|
export { createHandlerRegistry } from "./handlers.js";
|
|
8
8
|
export { html } from "./html.js";
|
|
9
|
-
export { AsyncLoader } from "./loader.js";
|
|
9
|
+
export { Loader, AsyncLoader } from "./loader.js";
|
|
10
10
|
export { createPartialRegistry } from "./partials.js";
|
|
11
11
|
export { createRegistryStore } from "./registry-store.js";
|
|
12
12
|
export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
|
package/src/loader.js
CHANGED
|
@@ -5,7 +5,7 @@ import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attri
|
|
|
5
5
|
|
|
6
6
|
const inlineBindingPrefix = "__async:inline:";
|
|
7
7
|
|
|
8
|
-
export function
|
|
8
|
+
export function Loader({ root, signals, handlers, server, router, cache, attributes } = {}) {
|
|
9
9
|
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
10
10
|
const rootNode = root ?? documentRef;
|
|
11
11
|
const signalRegistry = signals ?? createSignalRegistry();
|
|
@@ -19,6 +19,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
19
19
|
const boundaryState = new WeakMap();
|
|
20
20
|
const renderingBoundaries = new WeakSet();
|
|
21
21
|
const inlineBindings = new Map();
|
|
22
|
+
const scopedCleanups = new WeakMap();
|
|
22
23
|
let inlineBindingCounter = 0;
|
|
23
24
|
let destroyed = false;
|
|
24
25
|
|
|
@@ -53,6 +54,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
53
54
|
if (!boundary) {
|
|
54
55
|
throw new Error(`Boundary "${boundaryId}" was not found.`);
|
|
55
56
|
}
|
|
57
|
+
cleanupChildren(boundary);
|
|
56
58
|
boundary.replaceChildren(toFragment(fragmentOrTemplate, documentRef));
|
|
57
59
|
api.scan(boundary);
|
|
58
60
|
return boundary;
|
|
@@ -69,11 +71,12 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
69
71
|
cache: api.cache,
|
|
70
72
|
attributes: attributeConfig
|
|
71
73
|
});
|
|
74
|
+
cleanupChildren(target);
|
|
72
75
|
target.replaceChildren(toFragment(rendered.html, target.ownerDocument));
|
|
73
76
|
api.scan(target);
|
|
74
77
|
rendered.mount(target);
|
|
75
78
|
rendered.visible(target, api._observeVisible);
|
|
76
|
-
|
|
79
|
+
addCleanup(rendered.cleanup, target, "children");
|
|
77
80
|
return rendered;
|
|
78
81
|
},
|
|
79
82
|
|
|
@@ -83,7 +86,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
83
86
|
}
|
|
84
87
|
destroyed = true;
|
|
85
88
|
for (const cleanup of [...cleanups]) {
|
|
86
|
-
cleanup
|
|
89
|
+
runCleanup(cleanup);
|
|
87
90
|
}
|
|
88
91
|
cleanups.clear();
|
|
89
92
|
},
|
|
@@ -104,6 +107,13 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
|
|
110
|
+
api.server?._setContext?.({
|
|
111
|
+
signals: signalRegistry,
|
|
112
|
+
handlers: handlerRegistry,
|
|
113
|
+
loader: api,
|
|
114
|
+
router: api.router,
|
|
115
|
+
cache: api.cache
|
|
116
|
+
});
|
|
107
117
|
|
|
108
118
|
function bindEventAttributes(scope) {
|
|
109
119
|
for (const element of elementsIn(scope)) {
|
|
@@ -152,7 +162,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
152
162
|
};
|
|
153
163
|
|
|
154
164
|
element.addEventListener(eventName, listener);
|
|
155
|
-
|
|
165
|
+
addCleanup(() => element.removeEventListener(eventName, listener), element);
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
function bindSignalAttributes(scope) {
|
|
@@ -187,6 +197,12 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
187
197
|
bindSignal(element, `attr:${attr}:${path}`, path, (value) => updateAttribute(element, attr, value));
|
|
188
198
|
continue;
|
|
189
199
|
}
|
|
200
|
+
if (signalName.startsWith("prop:")) {
|
|
201
|
+
const prop = signalName.slice("prop:".length);
|
|
202
|
+
const path = element.getAttribute(name);
|
|
203
|
+
bindSignal(element, `prop:${prop}:${path}`, path, (value) => updateProperty(element, prop, value));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
190
206
|
if (signalName.startsWith("class:")) {
|
|
191
207
|
const className = signalName.slice("class:".length);
|
|
192
208
|
const path = element.getAttribute(name);
|
|
@@ -255,7 +271,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
255
271
|
|
|
256
272
|
const read = () => readBinding(path, options);
|
|
257
273
|
apply(read());
|
|
258
|
-
|
|
274
|
+
addCleanup(subscribeBinding(path, () => apply(read())), element);
|
|
259
275
|
}
|
|
260
276
|
|
|
261
277
|
function bindValueWriter(element, path) {
|
|
@@ -319,7 +335,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
319
335
|
cleanup: signalRegistry.subscribe(`${id}.$status`, () => renderBoundary(boundary))
|
|
320
336
|
};
|
|
321
337
|
boundaryState.set(boundary, state);
|
|
322
|
-
|
|
338
|
+
addCleanup(state.cleanup, boundary);
|
|
323
339
|
}
|
|
324
340
|
renderBoundary(boundary);
|
|
325
341
|
}
|
|
@@ -335,6 +351,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
335
351
|
if (!template) {
|
|
336
352
|
return;
|
|
337
353
|
}
|
|
354
|
+
cleanupChildren(boundary);
|
|
338
355
|
boundary.replaceChildren(template.content.cloneNode(true));
|
|
339
356
|
renderingBoundaries.add(boundary);
|
|
340
357
|
try {
|
|
@@ -368,7 +385,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
368
385
|
continue;
|
|
369
386
|
}
|
|
370
387
|
visibleElements.add(element);
|
|
371
|
-
|
|
388
|
+
addCleanup(observeVisible(element, () => runPseudo(element, ref)), element);
|
|
372
389
|
}
|
|
373
390
|
}
|
|
374
391
|
|
|
@@ -398,7 +415,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
398
415
|
});
|
|
399
416
|
for (const result of results) {
|
|
400
417
|
if (typeof result === "function") {
|
|
401
|
-
|
|
418
|
+
addCleanup(result, element);
|
|
402
419
|
}
|
|
403
420
|
}
|
|
404
421
|
} catch (error) {
|
|
@@ -430,13 +447,72 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
430
447
|
|
|
431
448
|
function assertActive() {
|
|
432
449
|
if (destroyed) {
|
|
433
|
-
throw new Error("
|
|
450
|
+
throw new Error("Loader has been destroyed.");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function addCleanup(cleanup, owner, mode = "self") {
|
|
455
|
+
if (typeof cleanup !== "function") {
|
|
456
|
+
return cleanup;
|
|
457
|
+
}
|
|
458
|
+
cleanups.add(cleanup);
|
|
459
|
+
if (owner) {
|
|
460
|
+
const records = scopedCleanups.get(owner) ?? [];
|
|
461
|
+
records.push({ cleanup, mode });
|
|
462
|
+
scopedCleanups.set(owner, records);
|
|
463
|
+
}
|
|
464
|
+
return cleanup;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function runCleanup(cleanup) {
|
|
468
|
+
if (typeof cleanup !== "function" || !cleanups.has(cleanup)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
cleanups.delete(cleanup);
|
|
472
|
+
cleanup();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function cleanupChildren(container) {
|
|
476
|
+
runScopedCleanups(container, "children");
|
|
477
|
+
for (const child of [...(container.childNodes ?? [])]) {
|
|
478
|
+
cleanupNode(child);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function cleanupNode(node) {
|
|
483
|
+
if (node.nodeType !== 1) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
for (const element of elementsIn(node)) {
|
|
487
|
+
runScopedCleanups(element);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function runScopedCleanups(element, mode) {
|
|
492
|
+
const records = scopedCleanups.get(element);
|
|
493
|
+
if (!records) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const remaining = [];
|
|
497
|
+
for (const record of records) {
|
|
498
|
+
if (mode && record.mode !== mode) {
|
|
499
|
+
remaining.push(record);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
runCleanup(record.cleanup);
|
|
503
|
+
}
|
|
504
|
+
if (remaining.length > 0) {
|
|
505
|
+
scopedCleanups.set(element, remaining);
|
|
506
|
+
} else {
|
|
507
|
+
scopedCleanups.delete(element);
|
|
434
508
|
}
|
|
435
509
|
}
|
|
436
510
|
|
|
437
511
|
return api;
|
|
438
512
|
}
|
|
439
513
|
|
|
514
|
+
export const AsyncLoader = Loader;
|
|
515
|
+
|
|
440
516
|
function normalizeClassTokens(value, tokens = new Set()) {
|
|
441
517
|
if (value == null || value === false) {
|
|
442
518
|
return tokens;
|
|
@@ -581,6 +657,14 @@ function updateAttribute(element, attr, value) {
|
|
|
581
657
|
}
|
|
582
658
|
}
|
|
583
659
|
|
|
660
|
+
function updateProperty(element, prop, value) {
|
|
661
|
+
if (value == null) {
|
|
662
|
+
element[prop] = "";
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
element[prop] = value;
|
|
666
|
+
}
|
|
667
|
+
|
|
584
668
|
function selectAll(scope, selector) {
|
|
585
669
|
const elements = [];
|
|
586
670
|
if (scope?.nodeType === 1 && scope.matches?.(selector)) {
|
package/src/partials.js
CHANGED
package/src/registry-store.js
CHANGED
package/src/router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Loader } from "./loader.js";
|
|
2
2
|
import { createHandlerRegistry } from "./handlers.js";
|
|
3
3
|
import { createSignalRegistry } from "./signals.js";
|
|
4
4
|
import { applyServerResult } from "./server.js";
|
|
@@ -41,6 +41,15 @@ export function createRouteRegistry(initialMap = {}, options = {}) {
|
|
|
41
41
|
return registry;
|
|
42
42
|
},
|
|
43
43
|
|
|
44
|
+
unregister(pattern) {
|
|
45
|
+
assertPattern(pattern);
|
|
46
|
+
const index = routes.findIndex((candidate) => candidate.pattern === pattern);
|
|
47
|
+
if (index !== -1) {
|
|
48
|
+
routes.splice(index, 1);
|
|
49
|
+
}
|
|
50
|
+
return entries.delete(pattern);
|
|
51
|
+
},
|
|
52
|
+
|
|
44
53
|
match(url) {
|
|
45
54
|
const path = toUrl(url).pathname;
|
|
46
55
|
for (const candidate of routes) {
|
|
@@ -119,7 +128,7 @@ export function createRouter({
|
|
|
119
128
|
const attributeConfig = normalizeAttributeConfig(attributes ?? loader?.attributes);
|
|
120
129
|
const loaderInstance =
|
|
121
130
|
loader ??
|
|
122
|
-
|
|
131
|
+
Loader({
|
|
123
132
|
root: rootNode,
|
|
124
133
|
signals: signalRegistry,
|
|
125
134
|
handlers: handlerRegistry,
|