@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/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 handlers = new Map();
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.registerMany(builtInHandlers);
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("&", "&amp;")
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
- export function AsyncLoader({ root, signals, handlers, server, router, cache } = {}) {
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 selectAll(scope, "[data-async-container], *")) {
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
- if (!name.startsWith("on:")) {
114
+ const eventName = matchAttribute(name, attributeConfig, "on");
115
+ if (!eventName) {
96
116
  continue;
97
117
  }
98
- const eventName = name.slice(3);
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 selectAll(scope, "[data-async-text]")) {
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
- if (name.startsWith("data-async-attr:")) {
161
- const attr = name.slice("data-async-attr:".length);
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, `attr:${attr}:${path}`, path, (value) => updateAttribute(element, attr, value));
167
+ bindSignal(element, `text:${path}`, path, (value) => {
168
+ element.textContent = value ?? "";
169
+ });
170
+ continue;
164
171
  }
165
- if (name.startsWith("data-async-class:")) {
166
- const className = name.slice("data-async-class:".length);
172
+ if (signalName === "value") {
167
173
  const path = element.getAttribute(name);
168
- bindSignal(element, `class:${className}:${path}`, path, (value) => {
169
- element.classList.toggle(className, Boolean(value));
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 bindSignal(element, key, path, apply) {
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
- apply(signalRegistry.get(path));
185
- cleanups.add(signalRegistry.subscribe(path, apply));
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
- signalRegistry.set(path, element.value);
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 selectAll(scope, "[data-async-boundary]")) {
303
+ for (const boundary of elementsIn(scope)) {
200
304
  if (renderingBoundaries.has(boundary)) {
201
305
  continue;
202
306
  }
203
- const id = boundary.getAttribute("data-async-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 selectAll(scope, "[on\\:mount]")) {
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
- runPseudo(element, element.getAttribute("on:mount"));
357
+ for (const ref of refs) {
358
+ runPseudo(element, ref);
359
+ }
247
360
  }
248
361
 
249
- for (const element of selectAll(scope, "[on\\:visible]")) {
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, element.getAttribute("on:visible"))));
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 collectBoundaryTemplates(boundary, id) {
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.getAttribute("data-async-loading") === id) {
547
+ if (readAttribute(template, attributeConfig, "async", "loading") === id) {
316
548
  templates.loading = template;
317
549
  }
318
- if (template.getAttribute("data-async-ready") === id) {
550
+ if (readAttribute(template, attributeConfig, "async", "ready") === id) {
319
551
  templates.ready = template;
320
552
  }
321
- if (template.getAttribute("data-async-error") === id) {
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 findBoundary(root, boundaryId) {
362
- const selector = `[data-async-boundary="${String(boundaryId).replaceAll('"', '\\"')}"]`;
363
- if (root?.nodeType === 1 && root.matches?.(selector)) {
364
- return root;
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 root?.querySelector?.(selector);
603
+ return null;
367
604
  }
368
605
 
369
606
  function toFragment(value, documentRef) {