@async/framework 0.3.0 → 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 +13 -0
- package/README.md +181 -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 +3912 -0
- package/package.json +9 -5
- package/src/attributes.js +2 -0
- package/src/component.js +83 -15
- package/src/html.js +99 -6
- package/src/loader.js +223 -12
- package/src/partials.js +15 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "No-build AsyncLoader 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",
|
|
@@ -28,19 +28,21 @@
|
|
|
28
28
|
"web-framework"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
|
-
"unpkg": "./
|
|
31
|
+
"unpkg": "./framework.js",
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
|
-
"unpkg": "./
|
|
35
|
-
"browser": "./
|
|
34
|
+
"unpkg": "./framework.js",
|
|
35
|
+
"browser": "./framework.js",
|
|
36
36
|
"import": "./src/index.js",
|
|
37
37
|
"default": "./src/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./framework.js": "./framework.js",
|
|
39
40
|
"./package.json": "./package.json"
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"CHANGELOG.md",
|
|
43
44
|
"README.md",
|
|
45
|
+
"framework.js",
|
|
44
46
|
"src",
|
|
45
47
|
"examples",
|
|
46
48
|
"LICENSE",
|
|
@@ -48,10 +50,12 @@
|
|
|
48
50
|
],
|
|
49
51
|
"scripts": {
|
|
50
52
|
"async-pipeline": "async-pipeline",
|
|
53
|
+
"bundle": "node scripts/build-framework-bundle.js",
|
|
54
|
+
"bundle:check": "node scripts/build-framework-bundle.js --check",
|
|
51
55
|
"docs:build": "node scripts/build-pages.js",
|
|
52
56
|
"examples": "node --test tests/examples.test.js",
|
|
53
57
|
"examples:check": "node --test tests/examples.test.js",
|
|
54
|
-
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
58
|
+
"pack:check": "pnpm run bundle:check && npm pack --dry-run --ignore-scripts",
|
|
55
59
|
"pipeline:github:check": "async-pipeline github check",
|
|
56
60
|
"pipeline:github:generate": "async-pipeline github generate",
|
|
57
61
|
"pipeline:pages": "async-pipeline run pages",
|
package/src/attributes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const defaultPrefixes = Object.freeze({
|
|
2
2
|
async: ["async:"],
|
|
3
|
+
class: ["class:"],
|
|
3
4
|
signal: ["signal:"],
|
|
4
5
|
on: ["on:"]
|
|
5
6
|
});
|
|
@@ -11,6 +12,7 @@ export function defineAttributeConfig(config = {}) {
|
|
|
11
12
|
export function normalizeAttributeConfig(config = {}) {
|
|
12
13
|
return {
|
|
13
14
|
async: normalizePrefixes(config.async, defaultPrefixes.async),
|
|
15
|
+
class: normalizePrefixes(config.class, defaultPrefixes.class),
|
|
14
16
|
signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
|
|
15
17
|
on: normalizePrefixes(config.on, defaultPrefixes.on)
|
|
16
18
|
};
|
package/src/component.js
CHANGED
|
@@ -71,29 +71,46 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
71
71
|
|
|
72
72
|
const scope = `${parentScope}.${componentName(Component)}.${++componentCounter}`;
|
|
73
73
|
const cleanups = [];
|
|
74
|
-
const
|
|
74
|
+
const attachHooks = [];
|
|
75
75
|
const visibleHooks = [];
|
|
76
|
+
const destroyHooks = [];
|
|
77
|
+
const bindingIds = [];
|
|
76
78
|
const context = createComponentContext({
|
|
77
79
|
runtime,
|
|
78
80
|
scope,
|
|
79
81
|
cleanups,
|
|
80
|
-
|
|
81
|
-
visibleHooks
|
|
82
|
+
attachHooks,
|
|
83
|
+
visibleHooks,
|
|
84
|
+
destroyHooks
|
|
82
85
|
});
|
|
83
86
|
|
|
84
87
|
const output = Component.call(context, props);
|
|
85
|
-
const html = renderTemplate(output
|
|
88
|
+
const html = renderTemplate(output, {
|
|
89
|
+
attributes: runtime.attributes,
|
|
90
|
+
signals: runtime.signals,
|
|
91
|
+
bind(value) {
|
|
92
|
+
const id = runtime.loader?._registerBinding?.(value);
|
|
93
|
+
if (!id) {
|
|
94
|
+
throw new Error("Inline template bindings require an AsyncLoader.");
|
|
95
|
+
}
|
|
96
|
+
bindingIds.push(id);
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
86
100
|
|
|
87
101
|
return {
|
|
88
102
|
html,
|
|
89
|
-
|
|
90
|
-
for (const hook of
|
|
103
|
+
attach(target) {
|
|
104
|
+
for (const hook of attachHooks) {
|
|
91
105
|
const cleanup = hook(target);
|
|
92
106
|
if (typeof cleanup === "function") {
|
|
93
107
|
cleanups.push(cleanup);
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
110
|
},
|
|
111
|
+
mount(target) {
|
|
112
|
+
this.attach(target);
|
|
113
|
+
},
|
|
97
114
|
visible(target, observeVisible) {
|
|
98
115
|
for (const hook of visibleHooks) {
|
|
99
116
|
const cleanup = observeVisible(target, hook);
|
|
@@ -103,15 +120,24 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
103
120
|
}
|
|
104
121
|
},
|
|
105
122
|
cleanup() {
|
|
123
|
+
while (destroyHooks.length > 0) {
|
|
124
|
+
destroyHooks.pop()?.();
|
|
125
|
+
}
|
|
106
126
|
while (cleanups.length > 0) {
|
|
107
127
|
cleanups.pop()?.();
|
|
108
128
|
}
|
|
129
|
+
while (bindingIds.length > 0) {
|
|
130
|
+
runtime.loader?._releaseBinding?.(bindingIds.pop());
|
|
131
|
+
}
|
|
109
132
|
}
|
|
110
133
|
};
|
|
111
134
|
}
|
|
112
135
|
|
|
113
|
-
function createComponentContext({ runtime, scope, cleanups,
|
|
136
|
+
function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks }) {
|
|
114
137
|
const { signals, handlers, loader, server, router, cache } = runtime;
|
|
138
|
+
const generatedHandlers = new WeakMap();
|
|
139
|
+
let generatedHandlerCounter = 0;
|
|
140
|
+
let generatedSignalCounter = 0;
|
|
115
141
|
const context = {
|
|
116
142
|
scope,
|
|
117
143
|
signals,
|
|
@@ -122,6 +148,9 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
122
148
|
cache,
|
|
123
149
|
|
|
124
150
|
signal(name, initial) {
|
|
151
|
+
if (arguments.length === 1) {
|
|
152
|
+
return signals.ensure(scoped(scope, `signal.${++generatedSignalCounter}`), name);
|
|
153
|
+
}
|
|
125
154
|
return signals.ensure(scoped(scope, name), initial);
|
|
126
155
|
},
|
|
127
156
|
|
|
@@ -150,31 +179,70 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
150
179
|
},
|
|
151
180
|
|
|
152
181
|
handler(name, fn) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
182
|
+
if (typeof name === "function" && fn === undefined) {
|
|
183
|
+
const inlineFn = name;
|
|
184
|
+
if (generatedHandlers.has(inlineFn)) {
|
|
185
|
+
return generatedHandlers.get(inlineFn);
|
|
186
|
+
}
|
|
187
|
+
const id = registerScopedHandler(`handler.${++generatedHandlerCounter}`, inlineFn);
|
|
188
|
+
generatedHandlers.set(inlineFn, id);
|
|
189
|
+
return id;
|
|
190
|
+
}
|
|
191
|
+
if (typeof fn !== "function") {
|
|
192
|
+
throw new TypeError("this.handler(name, fn) or this.handler(fn) requires a function.");
|
|
193
|
+
}
|
|
194
|
+
return registerScopedHandler(name, fn);
|
|
158
195
|
},
|
|
159
196
|
|
|
160
197
|
render(Child, childProps = {}) {
|
|
161
198
|
const child = renderComponent(Child, childProps, runtime, scope);
|
|
162
199
|
cleanups.push(child.cleanup);
|
|
163
|
-
|
|
200
|
+
attachHooks.push((target) => child.attach(target));
|
|
164
201
|
visibleHooks.push((target) => child.visible(target, loader._observeVisible));
|
|
165
202
|
return rawHtml(child.html);
|
|
166
203
|
},
|
|
167
204
|
|
|
205
|
+
on(eventName, fn) {
|
|
206
|
+
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
207
|
+
throw new TypeError("Component lifecycle event must be a non-empty string.");
|
|
208
|
+
}
|
|
209
|
+
if (typeof fn !== "function") {
|
|
210
|
+
throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
|
|
211
|
+
}
|
|
212
|
+
const event = eventName === "mount" ? "attach" : eventName;
|
|
213
|
+
if (event === "attach") {
|
|
214
|
+
attachHooks.push((target) => fn.call(context, target));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (event === "visible") {
|
|
218
|
+
visibleHooks.push((target) => fn.call(context, target));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (event === "destroy") {
|
|
222
|
+
destroyHooks.push(() => fn.call(context));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Unsupported component lifecycle event "${eventName}".`);
|
|
226
|
+
},
|
|
227
|
+
|
|
168
228
|
onMount(fn) {
|
|
169
|
-
|
|
229
|
+
context.on("attach", fn);
|
|
170
230
|
},
|
|
171
231
|
|
|
172
232
|
onVisible(fn) {
|
|
173
|
-
|
|
233
|
+
context.on("visible", fn);
|
|
174
234
|
}
|
|
175
235
|
};
|
|
176
236
|
|
|
177
237
|
return context;
|
|
238
|
+
|
|
239
|
+
function registerScopedHandler(name, fn) {
|
|
240
|
+
const id = scoped(scope, name);
|
|
241
|
+
handlers.register(id, function runComponentHandler(handlerContext) {
|
|
242
|
+
return fn.call({ ...context, ...handlerContext }, handlerContext);
|
|
243
|
+
});
|
|
244
|
+
return id;
|
|
245
|
+
}
|
|
178
246
|
}
|
|
179
247
|
|
|
180
248
|
function scoped(scope, name) {
|
package/src/html.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isSignalRef } from "./signals.js";
|
|
2
|
+
import { attributeName, matchAttribute, normalizeAttributeConfig } from "./attributes.js";
|
|
2
3
|
|
|
3
4
|
const templateKind = Symbol.for("@async/framework.template");
|
|
4
5
|
const rawKind = Symbol.for("@async/framework.rawHtml");
|
|
@@ -22,29 +23,36 @@ export function rawHtml(value) {
|
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function renderTemplate(value) {
|
|
26
|
+
export function renderTemplate(value, options = {}) {
|
|
26
27
|
if (isTemplateResult(value)) {
|
|
28
|
+
const context = createRenderContext(options);
|
|
27
29
|
let output = "";
|
|
28
30
|
for (let index = 0; index < value.strings.length; index += 1) {
|
|
29
31
|
output += value.strings[index];
|
|
30
32
|
if (index < value.values.length) {
|
|
31
|
-
output += renderValue(value.values[index]
|
|
33
|
+
output += renderValue(value.values[index], {
|
|
34
|
+
...context,
|
|
35
|
+
attribute: readAttributeContext(value.strings[index])
|
|
36
|
+
});
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
return output;
|
|
35
40
|
}
|
|
36
|
-
return renderValue(value);
|
|
41
|
+
return renderValue(value, createRenderContext(options));
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
function renderValue(value) {
|
|
44
|
+
function renderValue(value, context = createRenderContext()) {
|
|
40
45
|
if (value?.[rawKind]) {
|
|
41
46
|
return value.html;
|
|
42
47
|
}
|
|
43
48
|
if (isTemplateResult(value)) {
|
|
44
|
-
return renderTemplate(value);
|
|
49
|
+
return renderTemplate(value, context);
|
|
50
|
+
}
|
|
51
|
+
if (context.attribute) {
|
|
52
|
+
return renderAttributeValue(value, context);
|
|
45
53
|
}
|
|
46
54
|
if (Array.isArray(value)) {
|
|
47
|
-
return value.map(renderValue).join("");
|
|
55
|
+
return value.map((item) => renderValue(item, context)).join("");
|
|
48
56
|
}
|
|
49
57
|
if (isSignalRef(value)) {
|
|
50
58
|
return escapeHtml(value.value);
|
|
@@ -55,6 +63,91 @@ function renderValue(value) {
|
|
|
55
63
|
return escapeHtml(value);
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
function renderAttributeValue(value, context) {
|
|
67
|
+
const signalName = matchAttribute(context.attribute.name, context.attributes, "signal");
|
|
68
|
+
const className = matchAttribute(context.attribute.name, context.attributes, "class");
|
|
69
|
+
const signalPath = signalPathFor(value, context);
|
|
70
|
+
|
|
71
|
+
if (context.attribute.name === "value" && signalPath) {
|
|
72
|
+
const currentValue = readSignalValue(value, context);
|
|
73
|
+
const signalValueAttribute = attributeName(context.attributes, "signal", "value");
|
|
74
|
+
return `${escapeHtml(currentValue)}${context.attribute.quote} ${signalValueAttribute}=${context.attribute.quote}${escapeHtml(signalPath)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (signalName != null || className != null) {
|
|
78
|
+
if (signalPath) {
|
|
79
|
+
return escapeHtml(signalPath);
|
|
80
|
+
}
|
|
81
|
+
if (isInlineBindingValue(value)) {
|
|
82
|
+
return escapeHtml(registerInlineBinding(value, context));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return renderValueAsAttributeLiteral(value, context);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderValueAsAttributeLiteral(value, context) {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return value.map((item) => renderValueAsAttributeLiteral(item, context)).join("");
|
|
92
|
+
}
|
|
93
|
+
if (isSignalRef(value)) {
|
|
94
|
+
return escapeHtml(value.value);
|
|
95
|
+
}
|
|
96
|
+
if (value == null || value === false) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
return escapeHtml(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createRenderContext(options = {}) {
|
|
103
|
+
return {
|
|
104
|
+
...options,
|
|
105
|
+
attributes: normalizeAttributeConfig(options.attributes)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAttributeContext(source) {
|
|
110
|
+
const match = source.match(/(?:^|[\s<])([^\s"'=<>`]+)\s*=\s*(["'])$/);
|
|
111
|
+
if (!match) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
name: match[1],
|
|
116
|
+
quote: match[2]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function signalPathFor(value, context) {
|
|
121
|
+
if (isSignalRef(value)) {
|
|
122
|
+
return value.id;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === "string" && context.signals?.has?.(value)) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readSignalValue(value, context) {
|
|
131
|
+
if (isSignalRef(value)) {
|
|
132
|
+
return value.value;
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === "string" && context.signals?.has?.(value)) {
|
|
135
|
+
return context.signals.get(value);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isInlineBindingValue(value) {
|
|
141
|
+
return Boolean(value && typeof value === "object");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function registerInlineBinding(value, context) {
|
|
145
|
+
if (typeof context.bind !== "function") {
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
return context.bind(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
58
151
|
export function escapeHtml(value) {
|
|
59
152
|
return String(value)
|
|
60
153
|
.replaceAll("&", "&")
|
package/src/loader.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { renderComponent } from "./component.js";
|
|
2
2
|
import { createHandlerRegistry } from "./handlers.js";
|
|
3
|
-
import { createSignalRegistry } from "./signals.js";
|
|
3
|
+
import { createSignalRegistry, isSignalRef } from "./signals.js";
|
|
4
4
|
import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attributes.js";
|
|
5
5
|
|
|
6
|
+
const inlineBindingPrefix = "__async:inline:";
|
|
7
|
+
|
|
6
8
|
export function AsyncLoader({ root, signals, handlers, server, router, cache, attributes } = {}) {
|
|
7
9
|
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
8
10
|
const rootNode = root ?? documentRef;
|
|
@@ -16,6 +18,8 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
16
18
|
const visibleElements = new WeakSet();
|
|
17
19
|
const boundaryState = new WeakMap();
|
|
18
20
|
const renderingBoundaries = new WeakSet();
|
|
21
|
+
const inlineBindings = new Map();
|
|
22
|
+
let inlineBindingCounter = 0;
|
|
19
23
|
let destroyed = false;
|
|
20
24
|
|
|
21
25
|
const api = {
|
|
@@ -36,6 +40,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
36
40
|
scan(rootOrFragment = rootNode) {
|
|
37
41
|
assertActive();
|
|
38
42
|
bindSignalAttributes(rootOrFragment);
|
|
43
|
+
bindClassAttributes(rootOrFragment);
|
|
39
44
|
bindEventAttributes(rootOrFragment);
|
|
40
45
|
bindBoundaries(rootOrFragment);
|
|
41
46
|
runPseudoEvents(rootOrFragment);
|
|
@@ -85,6 +90,16 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
85
90
|
|
|
86
91
|
_observeVisible(target, fn) {
|
|
87
92
|
return observeVisible(target, fn);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
_registerBinding(value) {
|
|
96
|
+
const id = `${inlineBindingPrefix}${++inlineBindingCounter}`;
|
|
97
|
+
inlineBindings.set(id, value);
|
|
98
|
+
return id;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
_releaseBinding(id) {
|
|
102
|
+
inlineBindings.delete(id);
|
|
88
103
|
}
|
|
89
104
|
};
|
|
90
105
|
|
|
@@ -100,7 +115,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
100
115
|
if (!eventName) {
|
|
101
116
|
continue;
|
|
102
117
|
}
|
|
103
|
-
if (eventName === "mount" || eventName === "visible") {
|
|
118
|
+
if (eventName === "attach" || eventName === "mount" || eventName === "visible") {
|
|
104
119
|
continue;
|
|
105
120
|
}
|
|
106
121
|
bindEvent(element, eventName, element.getAttribute(name));
|
|
@@ -175,15 +190,62 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
175
190
|
if (signalName.startsWith("class:")) {
|
|
176
191
|
const className = signalName.slice("class:".length);
|
|
177
192
|
const path = element.getAttribute(name);
|
|
178
|
-
|
|
179
|
-
element
|
|
180
|
-
}
|
|
193
|
+
if (className === "" || className === "{}") {
|
|
194
|
+
bindClass(element, className, path);
|
|
195
|
+
} else {
|
|
196
|
+
bindSignal(element, `class:${className}:${path}`, path, (value) => {
|
|
197
|
+
element.classList.toggle(className, Boolean(value));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (signalName === "class") {
|
|
203
|
+
const path = element.getAttribute(name);
|
|
204
|
+
bindClass(element, "{}", path);
|
|
181
205
|
}
|
|
182
206
|
}
|
|
183
207
|
}
|
|
184
208
|
}
|
|
185
209
|
|
|
186
|
-
function
|
|
210
|
+
function bindClassAttributes(scope) {
|
|
211
|
+
for (const element of elementsIn(scope)) {
|
|
212
|
+
for (const name of element.getAttributeNames?.() ?? []) {
|
|
213
|
+
const className = matchAttribute(name, attributeConfig, "class");
|
|
214
|
+
if (className == null) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
bindClass(element, className, element.getAttribute(name));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function bindClass(element, className, path) {
|
|
223
|
+
if (className === "" || className === "{}") {
|
|
224
|
+
const staticClasses = readClassTokens(element);
|
|
225
|
+
let previous = new Set();
|
|
226
|
+
bindSignal(element, `class:{}:${path}`, path, (value) => {
|
|
227
|
+
const next = normalizeClassTokens(value);
|
|
228
|
+
const current = readClassTokens(element);
|
|
229
|
+
for (const token of previous) {
|
|
230
|
+
if (!next.has(token) && !staticClasses.has(token)) {
|
|
231
|
+
current.delete(token);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const token of next) {
|
|
235
|
+
current.add(token);
|
|
236
|
+
}
|
|
237
|
+
writeClassTokens(element, current);
|
|
238
|
+
previous = next;
|
|
239
|
+
}, { rawInline: true });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
bindSignal(element, `class:${className}:${path}`, path, (value) => {
|
|
244
|
+
updateClassToken(element, className, Boolean(value));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function bindSignal(element, key, path, apply, options = {}) {
|
|
187
249
|
const bound = signalBindings.get(element) ?? new Set();
|
|
188
250
|
if (bound.has(key)) {
|
|
189
251
|
return;
|
|
@@ -191,8 +253,9 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
191
253
|
bound.add(key);
|
|
192
254
|
signalBindings.set(element, bound);
|
|
193
255
|
|
|
194
|
-
|
|
195
|
-
|
|
256
|
+
const read = () => readBinding(path, options);
|
|
257
|
+
apply(read());
|
|
258
|
+
cleanups.add(subscribeBinding(path, () => apply(read())));
|
|
196
259
|
}
|
|
197
260
|
|
|
198
261
|
function bindValueWriter(element, path) {
|
|
@@ -200,11 +263,42 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
200
263
|
bindEvent(element, "change", `__async:set:${path}`);
|
|
201
264
|
if (!handlerRegistry.resolve(`__async:set:${path}`)) {
|
|
202
265
|
handlerRegistry.register(`__async:set:${path}`, function writeValue({ element }) {
|
|
203
|
-
|
|
266
|
+
writeBinding(path, element.value);
|
|
204
267
|
});
|
|
205
268
|
}
|
|
206
269
|
}
|
|
207
270
|
|
|
271
|
+
function readBinding(path, options = {}) {
|
|
272
|
+
if (isInlineBinding(path)) {
|
|
273
|
+
const value = inlineBindings.get(path);
|
|
274
|
+
return options.rawInline ? value : resolveInlineValue(value);
|
|
275
|
+
}
|
|
276
|
+
return signalRegistry.get(path);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function writeBinding(path, value) {
|
|
280
|
+
if (!isInlineBinding(path)) {
|
|
281
|
+
return signalRegistry.set(path, value);
|
|
282
|
+
}
|
|
283
|
+
const binding = inlineBindings.get(path);
|
|
284
|
+
if (isSignalRef(binding)) {
|
|
285
|
+
return binding.set(value);
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Inline binding "${path}" is not writable.`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function subscribeBinding(path, fn) {
|
|
291
|
+
if (!isInlineBinding(path)) {
|
|
292
|
+
return signalRegistry.subscribe(path, fn);
|
|
293
|
+
}
|
|
294
|
+
const cleanups = collectSignalRefs(inlineBindings.get(path)).map((ref) => ref.subscribe(fn));
|
|
295
|
+
return () => {
|
|
296
|
+
for (const cleanup of cleanups) {
|
|
297
|
+
cleanup();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
208
302
|
function bindBoundaries(scope) {
|
|
209
303
|
for (const boundary of elementsIn(scope)) {
|
|
210
304
|
if (renderingBoundaries.has(boundary)) {
|
|
@@ -252,15 +346,17 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
252
346
|
|
|
253
347
|
function runPseudoEvents(scope) {
|
|
254
348
|
for (const element of elementsIn(scope)) {
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
349
|
+
const refs = readPseudoRefs(element, ["attach", "mount"]);
|
|
350
|
+
if (refs.length === 0) {
|
|
257
351
|
continue;
|
|
258
352
|
}
|
|
259
353
|
if (mountedElements.has(element)) {
|
|
260
354
|
continue;
|
|
261
355
|
}
|
|
262
356
|
mountedElements.add(element);
|
|
263
|
-
|
|
357
|
+
for (const ref of refs) {
|
|
358
|
+
runPseudo(element, ref);
|
|
359
|
+
}
|
|
264
360
|
}
|
|
265
361
|
|
|
266
362
|
for (const element of elementsIn(scope)) {
|
|
@@ -276,6 +372,17 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
276
372
|
}
|
|
277
373
|
}
|
|
278
374
|
|
|
375
|
+
function readPseudoRefs(element, names) {
|
|
376
|
+
const refs = [];
|
|
377
|
+
for (const name of names) {
|
|
378
|
+
const ref = readAttribute(element, attributeConfig, "on", name);
|
|
379
|
+
if (ref != null) {
|
|
380
|
+
refs.push(ref);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return refs;
|
|
384
|
+
}
|
|
385
|
+
|
|
279
386
|
async function runPseudo(element, ref) {
|
|
280
387
|
try {
|
|
281
388
|
const results = await handlerRegistry.run(ref, {
|
|
@@ -330,6 +437,110 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
|
|
|
330
437
|
return api;
|
|
331
438
|
}
|
|
332
439
|
|
|
440
|
+
function normalizeClassTokens(value, tokens = new Set()) {
|
|
441
|
+
if (value == null || value === false) {
|
|
442
|
+
return tokens;
|
|
443
|
+
}
|
|
444
|
+
if (isSignalRef(value)) {
|
|
445
|
+
const signalValue = value.value;
|
|
446
|
+
if (signalValue === true) {
|
|
447
|
+
tokens.add(signalClassName(value.id));
|
|
448
|
+
return tokens;
|
|
449
|
+
}
|
|
450
|
+
return normalizeClassTokens(signalValue, tokens);
|
|
451
|
+
}
|
|
452
|
+
if (typeof value === "string") {
|
|
453
|
+
for (const token of value.split(/\s+/).filter(Boolean)) {
|
|
454
|
+
tokens.add(token);
|
|
455
|
+
}
|
|
456
|
+
return tokens;
|
|
457
|
+
}
|
|
458
|
+
if (Array.isArray(value)) {
|
|
459
|
+
for (const item of value) {
|
|
460
|
+
normalizeClassTokens(item, tokens);
|
|
461
|
+
}
|
|
462
|
+
return tokens;
|
|
463
|
+
}
|
|
464
|
+
if (typeof value === "object") {
|
|
465
|
+
for (const [token, enabled] of Object.entries(value)) {
|
|
466
|
+
const value = isSignalRef(enabled) ? enabled.value : enabled;
|
|
467
|
+
if (value) {
|
|
468
|
+
normalizeClassTokens(token, tokens);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return tokens;
|
|
472
|
+
}
|
|
473
|
+
if (value !== true) {
|
|
474
|
+
tokens.add(String(value));
|
|
475
|
+
}
|
|
476
|
+
return tokens;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function resolveInlineValue(value) {
|
|
480
|
+
if (isSignalRef(value)) {
|
|
481
|
+
return value.value;
|
|
482
|
+
}
|
|
483
|
+
if (Array.isArray(value)) {
|
|
484
|
+
return value.map(resolveInlineValue);
|
|
485
|
+
}
|
|
486
|
+
if (value && typeof value === "object") {
|
|
487
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveInlineValue(entry)]));
|
|
488
|
+
}
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function collectSignalRefs(value, refs = new Map()) {
|
|
493
|
+
if (isSignalRef(value)) {
|
|
494
|
+
refs.set(value.id, value);
|
|
495
|
+
return [...refs.values()];
|
|
496
|
+
}
|
|
497
|
+
if (Array.isArray(value)) {
|
|
498
|
+
for (const item of value) {
|
|
499
|
+
collectSignalRefs(item, refs);
|
|
500
|
+
}
|
|
501
|
+
return [...refs.values()];
|
|
502
|
+
}
|
|
503
|
+
if (value && typeof value === "object") {
|
|
504
|
+
for (const item of Object.values(value)) {
|
|
505
|
+
collectSignalRefs(item, refs);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return [...refs.values()];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function isInlineBinding(value) {
|
|
512
|
+
return typeof value === "string" && value.startsWith(inlineBindingPrefix);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function signalClassName(id) {
|
|
516
|
+
return id.split(".").at(-1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function updateClassToken(element, className, enabled) {
|
|
520
|
+
const tokens = readClassTokens(element);
|
|
521
|
+
for (const token of normalizeClassTokens(className)) {
|
|
522
|
+
if (enabled) {
|
|
523
|
+
tokens.add(token);
|
|
524
|
+
} else {
|
|
525
|
+
tokens.delete(token);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
writeClassTokens(element, tokens);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function readClassTokens(element) {
|
|
532
|
+
return normalizeClassTokens(element.getAttribute("class") ?? "");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function writeClassTokens(element, tokens) {
|
|
536
|
+
const value = [...tokens].join(" ");
|
|
537
|
+
if (value.length === 0) {
|
|
538
|
+
element.removeAttribute("class");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
element.setAttribute("class", value);
|
|
542
|
+
}
|
|
543
|
+
|
|
333
544
|
function collectBoundaryTemplates(boundary, id, attributeConfig) {
|
|
334
545
|
const templates = {};
|
|
335
546
|
for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
|