@c9up/aurora 0.1.26 → 0.1.27
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/dist/hydrate.js +44 -0
- package/dist/render.js +9 -2
- package/package.json +1 -1
- package/src/hydrate.ts +74 -0
- package/src/render.ts +12 -2
package/dist/hydrate.js
CHANGED
|
@@ -257,6 +257,11 @@ function hydrateTemplateResult(result, liveNodes, cleanups, mountHooks, markerCu
|
|
|
257
257
|
const syntheticRoot = {
|
|
258
258
|
childNodes: liveNodes,
|
|
259
259
|
};
|
|
260
|
+
// An attribute interpolating several slots — `class="static ${a} ${b}"` — is
|
|
261
|
+
// ONE attribute value built from all of them plus the static segments in
|
|
262
|
+
// between. Binding each slot on its own would have the last writer win and
|
|
263
|
+
// wipe the statics, which is what render.ts already avoids server-side.
|
|
264
|
+
const multiGroups = new Map();
|
|
260
265
|
for (let i = 0; i < tpl.slots.length; i++) {
|
|
261
266
|
const slot = tpl.slots[i];
|
|
262
267
|
const liveNode = resolvePathLive(syntheticRoot, slot.path, liveNodes);
|
|
@@ -270,8 +275,47 @@ function hydrateTemplateResult(result, liveNodes, cleanups, mountHooks, markerCu
|
|
|
270
275
|
}
|
|
271
276
|
continue;
|
|
272
277
|
}
|
|
278
|
+
if (slot.kind === "attr" && slot.staticParts !== undefined) {
|
|
279
|
+
collectMultiAttr(slot, liveNode, result.values[i], multiGroups);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
273
282
|
hydrateSlot(slot, liveNode, result.values[i], cleanups, mountHooks, markerCursor);
|
|
274
283
|
}
|
|
284
|
+
for (const group of multiGroups.values()) {
|
|
285
|
+
applyMultiAttrGroup(group, cleanups);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function collectMultiAttr(slot, el, value, groups) {
|
|
289
|
+
if (!slot.staticParts)
|
|
290
|
+
return;
|
|
291
|
+
const key = `${slot.name}::${slot.path.join(".")}`;
|
|
292
|
+
let group = groups.get(key);
|
|
293
|
+
if (!group) {
|
|
294
|
+
group = { el, name: slot.name, staticParts: slot.staticParts, values: [] };
|
|
295
|
+
groups.set(key, group);
|
|
296
|
+
}
|
|
297
|
+
group.values.push(value);
|
|
298
|
+
}
|
|
299
|
+
function applyMultiAttrGroup(group, cleanups) {
|
|
300
|
+
function join() {
|
|
301
|
+
let out = group.staticParts[0] ?? "";
|
|
302
|
+
for (let i = 0; i < group.values.length; i++) {
|
|
303
|
+
const v = group.values[i];
|
|
304
|
+
const resolved = isSignal(v) || typeof v === "function" ? v() : v;
|
|
305
|
+
out += resolved == null || resolved === false ? "" : String(resolved);
|
|
306
|
+
out += group.staticParts[i + 1] ?? "";
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
const hasReactive = group.values.some((v) => isSignal(v) || typeof v === "function");
|
|
311
|
+
if (hasReactive) {
|
|
312
|
+
// SSR already wrote the joined value; re-joining on every tick is what
|
|
313
|
+
// keeps the statics in place when only one part changes.
|
|
314
|
+
cleanups.push(effect(() => {
|
|
315
|
+
group.el.setAttribute(group.name, join());
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
// Fully static groups need nothing: SSR wrote the final value.
|
|
275
319
|
}
|
|
276
320
|
/**
|
|
277
321
|
* Resolve a slot's path against the LIVE DOM. The first index of the
|
package/dist/render.js
CHANGED
|
@@ -68,10 +68,17 @@ export function mount(result, cleanups, mounted, mountHooks) {
|
|
|
68
68
|
// the final string. Collect them in a first pass, attach effects
|
|
69
69
|
// after.
|
|
70
70
|
const multiGroups = new Map();
|
|
71
|
+
// Resolve EVERY slot's node before applying any of them. Applying a text
|
|
72
|
+
// slot inserts nodes into the fragment, which shifts the child indices the
|
|
73
|
+
// remaining paths were computed against — so a slot sitting after a nested
|
|
74
|
+
// template (`${Icon()}${label}`) used to resolve to the wrong node, or to
|
|
75
|
+
// none, and silently never bound. Fragments exist precisely so a component
|
|
76
|
+
// needs no wrapper element; they must not cost the slots that follow them.
|
|
77
|
+
const resolved = tpl.slots.map((slot) => resolvePath(fragment, slot.path));
|
|
71
78
|
for (let i = 0; i < tpl.slots.length; i++) {
|
|
72
79
|
const slot = tpl.slots[i];
|
|
73
|
-
const node =
|
|
74
|
-
if (node === null) {
|
|
80
|
+
const node = resolved[i];
|
|
81
|
+
if (node === null || node === undefined) {
|
|
75
82
|
// Path didn't resolve — skip this binding rather than crash (see
|
|
76
83
|
// resolvePath). Degrades to a dead binding; the surrounding render
|
|
77
84
|
// (and any command driving it) survives.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/hydrate.ts
CHANGED
|
@@ -358,6 +358,12 @@ function hydrateTemplateResult(
|
|
|
358
358
|
childNodes: liveNodes,
|
|
359
359
|
} as unknown as ParentNode;
|
|
360
360
|
|
|
361
|
+
// An attribute interpolating several slots — `class="static ${a} ${b}"` — is
|
|
362
|
+
// ONE attribute value built from all of them plus the static segments in
|
|
363
|
+
// between. Binding each slot on its own would have the last writer win and
|
|
364
|
+
// wipe the statics, which is what render.ts already avoids server-side.
|
|
365
|
+
const multiGroups = new Map<string, MultiAttrGroup>();
|
|
366
|
+
|
|
361
367
|
for (let i = 0; i < tpl.slots.length; i++) {
|
|
362
368
|
const slot = tpl.slots[i];
|
|
363
369
|
const liveNode = resolvePathLive(syntheticRoot, slot.path, liveNodes);
|
|
@@ -373,6 +379,15 @@ function hydrateTemplateResult(
|
|
|
373
379
|
}
|
|
374
380
|
continue;
|
|
375
381
|
}
|
|
382
|
+
if (slot.kind === "attr" && slot.staticParts !== undefined) {
|
|
383
|
+
collectMultiAttr(
|
|
384
|
+
slot,
|
|
385
|
+
liveNode as Element,
|
|
386
|
+
result.values[i],
|
|
387
|
+
multiGroups,
|
|
388
|
+
);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
376
391
|
hydrateSlot(
|
|
377
392
|
slot,
|
|
378
393
|
liveNode,
|
|
@@ -382,6 +397,65 @@ function hydrateTemplateResult(
|
|
|
382
397
|
markerCursor,
|
|
383
398
|
);
|
|
384
399
|
}
|
|
400
|
+
|
|
401
|
+
for (const group of multiGroups.values()) {
|
|
402
|
+
applyMultiAttrGroup(group, cleanups);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** One attribute whose value is assembled from several slots. */
|
|
407
|
+
interface MultiAttrGroup {
|
|
408
|
+
el: Element;
|
|
409
|
+
name: string;
|
|
410
|
+
staticParts: readonly string[];
|
|
411
|
+
values: unknown[];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function collectMultiAttr(
|
|
415
|
+
slot: AttrSlot,
|
|
416
|
+
el: Element,
|
|
417
|
+
value: unknown,
|
|
418
|
+
groups: Map<string, MultiAttrGroup>,
|
|
419
|
+
): void {
|
|
420
|
+
if (!slot.staticParts) return;
|
|
421
|
+
const key = `${slot.name}::${(slot.path as readonly number[]).join(".")}`;
|
|
422
|
+
let group = groups.get(key);
|
|
423
|
+
if (!group) {
|
|
424
|
+
group = { el, name: slot.name, staticParts: slot.staticParts, values: [] };
|
|
425
|
+
groups.set(key, group);
|
|
426
|
+
}
|
|
427
|
+
group.values.push(value);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function applyMultiAttrGroup(
|
|
431
|
+
group: MultiAttrGroup,
|
|
432
|
+
cleanups: Disposer[],
|
|
433
|
+
): void {
|
|
434
|
+
function join(): string {
|
|
435
|
+
let out = group.staticParts[0] ?? "";
|
|
436
|
+
for (let i = 0; i < group.values.length; i++) {
|
|
437
|
+
const v = group.values[i];
|
|
438
|
+
const resolved =
|
|
439
|
+
isSignal(v) || typeof v === "function" ? (v as () => unknown)() : v;
|
|
440
|
+
out += resolved == null || resolved === false ? "" : String(resolved);
|
|
441
|
+
out += group.staticParts[i + 1] ?? "";
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const hasReactive = group.values.some(
|
|
447
|
+
(v) => isSignal(v) || typeof v === "function",
|
|
448
|
+
);
|
|
449
|
+
if (hasReactive) {
|
|
450
|
+
// SSR already wrote the joined value; re-joining on every tick is what
|
|
451
|
+
// keeps the statics in place when only one part changes.
|
|
452
|
+
cleanups.push(
|
|
453
|
+
effect(() => {
|
|
454
|
+
group.el.setAttribute(group.name, join());
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
// Fully static groups need nothing: SSR wrote the final value.
|
|
385
459
|
}
|
|
386
460
|
|
|
387
461
|
/**
|
package/src/render.ts
CHANGED
|
@@ -104,10 +104,20 @@ export function mount(
|
|
|
104
104
|
// after.
|
|
105
105
|
const multiGroups = new Map<string, MultiAttrGroup>();
|
|
106
106
|
|
|
107
|
+
// Resolve EVERY slot's node before applying any of them. Applying a text
|
|
108
|
+
// slot inserts nodes into the fragment, which shifts the child indices the
|
|
109
|
+
// remaining paths were computed against — so a slot sitting after a nested
|
|
110
|
+
// template (`${Icon()}${label}`) used to resolve to the wrong node, or to
|
|
111
|
+
// none, and silently never bound. Fragments exist precisely so a component
|
|
112
|
+
// needs no wrapper element; they must not cost the slots that follow them.
|
|
113
|
+
const resolved: Array<Node | null> = tpl.slots.map((slot) =>
|
|
114
|
+
resolvePath(fragment, slot.path),
|
|
115
|
+
);
|
|
116
|
+
|
|
107
117
|
for (let i = 0; i < tpl.slots.length; i++) {
|
|
108
118
|
const slot = tpl.slots[i];
|
|
109
|
-
const node =
|
|
110
|
-
if (node === null) {
|
|
119
|
+
const node = resolved[i];
|
|
120
|
+
if (node === null || node === undefined) {
|
|
111
121
|
// Path didn't resolve — skip this binding rather than crash (see
|
|
112
122
|
// resolvePath). Degrades to a dead binding; the surrounding render
|
|
113
123
|
// (and any command driving it) survives.
|