@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/src/handlers.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
resolveServerCommandArguments,
|
|
5
5
|
unwrapServerResult
|
|
6
6
|
} from "./server.js";
|
|
7
|
+
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
7
8
|
|
|
8
9
|
const builtInTokens = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"]);
|
|
9
10
|
const builtInHandlers = {
|
|
@@ -18,10 +19,12 @@ const builtInHandlers = {
|
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
export function createHandlerRegistry(initialMap = {}) {
|
|
22
|
-
const
|
|
22
|
+
export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
23
|
+
const registryStore = options.registry ?? createRegistryStore();
|
|
24
|
+
const type = options.type ?? "handler";
|
|
25
|
+
const handlers = registryStore._map(type);
|
|
23
26
|
|
|
24
|
-
const registry = {
|
|
27
|
+
const registry = attachRegistryInspection({
|
|
25
28
|
register(id, fn) {
|
|
26
29
|
assertId(id);
|
|
27
30
|
if (typeof fn !== "function") {
|
|
@@ -90,14 +93,30 @@ export function createHandlerRegistry(initialMap = {}) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
return results;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
_adoptMany() {
|
|
99
|
+
return registry;
|
|
93
100
|
}
|
|
94
|
-
};
|
|
101
|
+
}, registryStore, type);
|
|
95
102
|
|
|
96
|
-
registry
|
|
103
|
+
registerBuiltIns(registry, handlers);
|
|
97
104
|
registry.registerMany(initialMap);
|
|
98
105
|
return registry;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
function registerBuiltIns(registry, handlers) {
|
|
109
|
+
for (const [id, fn] of Object.entries(builtInHandlers)) {
|
|
110
|
+
if (!handlers.has(id)) {
|
|
111
|
+
registry.register(id, fn);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (handlers.get(id) !== fn) {
|
|
115
|
+
throw new Error(`Handler "${id}" is already registered.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
export function parseHandlerRef(ref) {
|
|
102
121
|
if (typeof ref !== "string" || ref.trim().length === 0) {
|
|
103
122
|
throw new TypeError("Handler ref must be a non-empty string.");
|
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/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { asyncSignal } from "./async-signal.js";
|
|
2
2
|
export { Async, createApp, defineApp } from "./app.js";
|
|
3
|
+
export { attributeName, defineAttributeConfig } from "./attributes.js";
|
|
3
4
|
export { createCacheRegistry, defineCache } from "./cache.js";
|
|
4
5
|
export { component, createComponentRegistry, defineComponent } from "./component.js";
|
|
5
6
|
export { delay } from "./delay.js";
|
|
@@ -7,6 +8,7 @@ export { createHandlerRegistry } from "./handlers.js";
|
|
|
7
8
|
export { html } from "./html.js";
|
|
8
9
|
export { AsyncLoader } from "./loader.js";
|
|
9
10
|
export { createPartialRegistry } from "./partials.js";
|
|
11
|
+
export { createRegistryStore } from "./registry-store.js";
|
|
10
12
|
export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
|
|
11
13
|
export { createServerProxy, createServerRegistry } from "./server.js";
|
|
12
14
|
export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";
|
package/src/loader.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
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
|
+
import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attributes.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const inlineBindingPrefix = "__async:inline:";
|
|
7
|
+
|
|
8
|
+
export function AsyncLoader({ root, signals, handlers, server, router, cache, attributes } = {}) {
|
|
6
9
|
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
7
10
|
const rootNode = root ?? documentRef;
|
|
8
11
|
const signalRegistry = signals ?? createSignalRegistry();
|
|
9
12
|
const handlerRegistry = handlers ?? createHandlerRegistry();
|
|
13
|
+
const attributeConfig = normalizeAttributeConfig(attributes);
|
|
10
14
|
const cleanups = new Set();
|
|
11
15
|
const eventBindings = new WeakMap();
|
|
12
16
|
const signalBindings = new WeakMap();
|
|
@@ -14,6 +18,8 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
14
18
|
const visibleElements = new WeakSet();
|
|
15
19
|
const boundaryState = new WeakMap();
|
|
16
20
|
const renderingBoundaries = new WeakSet();
|
|
21
|
+
const inlineBindings = new Map();
|
|
22
|
+
let inlineBindingCounter = 0;
|
|
17
23
|
let destroyed = false;
|
|
18
24
|
|
|
19
25
|
const api = {
|
|
@@ -23,6 +29,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
23
29
|
server,
|
|
24
30
|
router,
|
|
25
31
|
cache,
|
|
32
|
+
attributes: attributeConfig,
|
|
26
33
|
|
|
27
34
|
start() {
|
|
28
35
|
assertActive();
|
|
@@ -33,6 +40,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
33
40
|
scan(rootOrFragment = rootNode) {
|
|
34
41
|
assertActive();
|
|
35
42
|
bindSignalAttributes(rootOrFragment);
|
|
43
|
+
bindClassAttributes(rootOrFragment);
|
|
36
44
|
bindEventAttributes(rootOrFragment);
|
|
37
45
|
bindBoundaries(rootOrFragment);
|
|
38
46
|
runPseudoEvents(rootOrFragment);
|
|
@@ -41,7 +49,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
41
49
|
|
|
42
50
|
swap(boundaryId, fragmentOrTemplate) {
|
|
43
51
|
assertActive();
|
|
44
|
-
const boundary = findBoundary(rootNode, boundaryId);
|
|
52
|
+
const boundary = findBoundary(rootNode, boundaryId, attributeConfig);
|
|
45
53
|
if (!boundary) {
|
|
46
54
|
throw new Error(`Boundary "${boundaryId}" was not found.`);
|
|
47
55
|
}
|
|
@@ -58,7 +66,8 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
58
66
|
loader: api,
|
|
59
67
|
server: api.server,
|
|
60
68
|
router: api.router,
|
|
61
|
-
cache: api.cache
|
|
69
|
+
cache: api.cache,
|
|
70
|
+
attributes: attributeConfig
|
|
62
71
|
});
|
|
63
72
|
target.replaceChildren(toFragment(rendered.html, target.ownerDocument));
|
|
64
73
|
api.scan(target);
|
|
@@ -81,22 +90,32 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
81
90
|
|
|
82
91
|
_observeVisible(target, fn) {
|
|
83
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);
|
|
84
103
|
}
|
|
85
104
|
};
|
|
86
105
|
|
|
87
106
|
signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
|
|
88
107
|
|
|
89
108
|
function bindEventAttributes(scope) {
|
|
90
|
-
for (const element of
|
|
109
|
+
for (const element of elementsIn(scope)) {
|
|
91
110
|
if (typeof element.getAttributeNames !== "function") {
|
|
92
111
|
continue;
|
|
93
112
|
}
|
|
94
113
|
for (const name of element.getAttributeNames()) {
|
|
95
|
-
|
|
114
|
+
const eventName = matchAttribute(name, attributeConfig, "on");
|
|
115
|
+
if (!eventName) {
|
|
96
116
|
continue;
|
|
97
117
|
}
|
|
98
|
-
|
|
99
|
-
if (eventName === "mount" || eventName === "visible") {
|
|
118
|
+
if (eventName === "attach" || eventName === "mount" || eventName === "visible") {
|
|
100
119
|
continue;
|
|
101
120
|
}
|
|
102
121
|
bindEvent(element, eventName, element.getAttribute(name));
|
|
@@ -137,43 +156,96 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
function bindSignalAttributes(scope) {
|
|
140
|
-
for (const element of
|
|
141
|
-
bindSignal(element, `text:${element.getAttribute("data-async-text")}`, element.getAttribute("data-async-text"), (value) => {
|
|
142
|
-
element.textContent = value ?? "";
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
for (const element of selectAll(scope, "[data-async-value]")) {
|
|
147
|
-
const path = element.getAttribute("data-async-value");
|
|
148
|
-
bindSignal(element, `value:${path}`, path, (value) => {
|
|
149
|
-
if ("value" in element && element.value !== String(value ?? "")) {
|
|
150
|
-
element.value = value ?? "";
|
|
151
|
-
} else if (!("value" in element)) {
|
|
152
|
-
element.setAttribute("value", value ?? "");
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
bindValueWriter(element, path);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const element of selectAll(scope, "*")) {
|
|
159
|
+
for (const element of elementsIn(scope)) {
|
|
159
160
|
for (const name of element.getAttributeNames?.() ?? []) {
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
const signalName = matchAttribute(name, attributeConfig, "signal");
|
|
162
|
+
if (!signalName) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (signalName === "text") {
|
|
162
166
|
const path = element.getAttribute(name);
|
|
163
|
-
bindSignal(element, `
|
|
167
|
+
bindSignal(element, `text:${path}`, path, (value) => {
|
|
168
|
+
element.textContent = value ?? "";
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
164
171
|
}
|
|
165
|
-
if (
|
|
166
|
-
const className = name.slice("data-async-class:".length);
|
|
172
|
+
if (signalName === "value") {
|
|
167
173
|
const path = element.getAttribute(name);
|
|
168
|
-
bindSignal(element, `
|
|
169
|
-
element.
|
|
174
|
+
bindSignal(element, `value:${path}`, path, (value) => {
|
|
175
|
+
if ("value" in element && element.value !== String(value ?? "")) {
|
|
176
|
+
element.value = value ?? "";
|
|
177
|
+
} else if (!("value" in element)) {
|
|
178
|
+
element.setAttribute("value", value ?? "");
|
|
179
|
+
}
|
|
170
180
|
});
|
|
181
|
+
bindValueWriter(element, path);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (signalName.startsWith("attr:")) {
|
|
185
|
+
const attr = signalName.slice("attr:".length);
|
|
186
|
+
const path = element.getAttribute(name);
|
|
187
|
+
bindSignal(element, `attr:${attr}:${path}`, path, (value) => updateAttribute(element, attr, value));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (signalName.startsWith("class:")) {
|
|
191
|
+
const className = signalName.slice("class:".length);
|
|
192
|
+
const path = element.getAttribute(name);
|
|
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);
|
|
171
205
|
}
|
|
172
206
|
}
|
|
173
207
|
}
|
|
174
208
|
}
|
|
175
209
|
|
|
176
|
-
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 = {}) {
|
|
177
249
|
const bound = signalBindings.get(element) ?? new Set();
|
|
178
250
|
if (bound.has(key)) {
|
|
179
251
|
return;
|
|
@@ -181,8 +253,9 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
181
253
|
bound.add(key);
|
|
182
254
|
signalBindings.set(element, bound);
|
|
183
255
|
|
|
184
|
-
|
|
185
|
-
|
|
256
|
+
const read = () => readBinding(path, options);
|
|
257
|
+
apply(read());
|
|
258
|
+
cleanups.add(subscribeBinding(path, () => apply(read())));
|
|
186
259
|
}
|
|
187
260
|
|
|
188
261
|
function bindValueWriter(element, path) {
|
|
@@ -190,19 +263,53 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
190
263
|
bindEvent(element, "change", `__async:set:${path}`);
|
|
191
264
|
if (!handlerRegistry.resolve(`__async:set:${path}`)) {
|
|
192
265
|
handlerRegistry.register(`__async:set:${path}`, function writeValue({ element }) {
|
|
193
|
-
|
|
266
|
+
writeBinding(path, element.value);
|
|
194
267
|
});
|
|
195
268
|
}
|
|
196
269
|
}
|
|
197
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
|
+
|
|
198
302
|
function bindBoundaries(scope) {
|
|
199
|
-
for (const boundary of
|
|
303
|
+
for (const boundary of elementsIn(scope)) {
|
|
200
304
|
if (renderingBoundaries.has(boundary)) {
|
|
201
305
|
continue;
|
|
202
306
|
}
|
|
203
|
-
const id = boundary
|
|
307
|
+
const id = readAttribute(boundary, attributeConfig, "async", "boundary");
|
|
308
|
+
if (id == null) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
204
311
|
if (!boundaryState.has(boundary)) {
|
|
205
|
-
const templates = collectBoundaryTemplates(boundary, id);
|
|
312
|
+
const templates = collectBoundaryTemplates(boundary, id, attributeConfig);
|
|
206
313
|
if (Object.keys(templates).length === 0 || !signalRegistry.has(id)) {
|
|
207
314
|
continue;
|
|
208
315
|
}
|
|
@@ -238,23 +345,44 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
238
345
|
}
|
|
239
346
|
|
|
240
347
|
function runPseudoEvents(scope) {
|
|
241
|
-
for (const element of
|
|
348
|
+
for (const element of elementsIn(scope)) {
|
|
349
|
+
const refs = readPseudoRefs(element, ["attach", "mount"]);
|
|
350
|
+
if (refs.length === 0) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
242
353
|
if (mountedElements.has(element)) {
|
|
243
354
|
continue;
|
|
244
355
|
}
|
|
245
356
|
mountedElements.add(element);
|
|
246
|
-
|
|
357
|
+
for (const ref of refs) {
|
|
358
|
+
runPseudo(element, ref);
|
|
359
|
+
}
|
|
247
360
|
}
|
|
248
361
|
|
|
249
|
-
for (const element of
|
|
362
|
+
for (const element of elementsIn(scope)) {
|
|
363
|
+
const ref = readAttribute(element, attributeConfig, "on", "visible");
|
|
364
|
+
if (ref == null) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
250
367
|
if (visibleElements.has(element)) {
|
|
251
368
|
continue;
|
|
252
369
|
}
|
|
253
370
|
visibleElements.add(element);
|
|
254
|
-
cleanups.add(observeVisible(element, () => runPseudo(element,
|
|
371
|
+
cleanups.add(observeVisible(element, () => runPseudo(element, ref)));
|
|
255
372
|
}
|
|
256
373
|
}
|
|
257
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
|
+
|
|
258
386
|
async function runPseudo(element, ref) {
|
|
259
387
|
try {
|
|
260
388
|
const results = await handlerRegistry.run(ref, {
|
|
@@ -309,16 +437,120 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache } =
|
|
|
309
437
|
return api;
|
|
310
438
|
}
|
|
311
439
|
|
|
312
|
-
function
|
|
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
|
+
|
|
544
|
+
function collectBoundaryTemplates(boundary, id, attributeConfig) {
|
|
313
545
|
const templates = {};
|
|
314
546
|
for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
|
|
315
|
-
if (template
|
|
547
|
+
if (readAttribute(template, attributeConfig, "async", "loading") === id) {
|
|
316
548
|
templates.loading = template;
|
|
317
549
|
}
|
|
318
|
-
if (template
|
|
550
|
+
if (readAttribute(template, attributeConfig, "async", "ready") === id) {
|
|
319
551
|
templates.ready = template;
|
|
320
552
|
}
|
|
321
|
-
if (template
|
|
553
|
+
if (readAttribute(template, attributeConfig, "async", "error") === id) {
|
|
322
554
|
templates.error = template;
|
|
323
555
|
}
|
|
324
556
|
}
|
|
@@ -358,12 +590,17 @@ function selectAll(scope, selector) {
|
|
|
358
590
|
return elements;
|
|
359
591
|
}
|
|
360
592
|
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
593
|
+
function elementsIn(scope) {
|
|
594
|
+
return selectAll(scope, "*");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function findBoundary(root, boundaryId, attributeConfig) {
|
|
598
|
+
for (const element of elementsIn(root)) {
|
|
599
|
+
if (readAttribute(element, attributeConfig, "async", "boundary") === String(boundaryId)) {
|
|
600
|
+
return element;
|
|
601
|
+
}
|
|
365
602
|
}
|
|
366
|
-
return
|
|
603
|
+
return null;
|
|
367
604
|
}
|
|
368
605
|
|
|
369
606
|
function toFragment(value, documentRef) {
|