@zoijs/core 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/core/mount.js +10 -3
- package/src/core/renderer.js +26 -5
- package/src/index.d.ts +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to Zoijs are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/), and Zoijs follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/) (see `VERSIONING.md`).
|
|
6
6
|
|
|
7
|
+
## [1.6.0] — 2026-06-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Hydration — `mount(component, target, { hydrate: true })`.** The client now
|
|
11
|
+
**adopts** server-rendered DOM in place instead of re-creating it: with `hydrate`,
|
|
12
|
+
`mount` reuses the existing elements inside `target` exactly — never cloning or
|
|
13
|
+
replacing them — and attaches event handlers and reactive attribute bindings to
|
|
14
|
+
those live nodes. Each dynamic child slot's server content is cleared (back to the
|
|
15
|
+
`<!--zoijs:[-->` start marker that [`@zoijs/ssr`](https://www.npmjs.com/package/@zoijs/ssr)
|
|
16
|
+
emits) and re-rendered in place; since the values match the server, there is no
|
|
17
|
+
visible change and no flash. This is the client half of full SSR — pair it with
|
|
18
|
+
`renderToString(component, { hydratable: true })`. The default `mount` path is
|
|
19
|
+
byte-for-byte unchanged (the option is additive), and the nine-function main surface
|
|
20
|
+
is the same. `@zoijs/ssr` re-exports this as `hydrate()`. See
|
|
21
|
+
[RFC 0008](docs/rfcs/0008-ssr.md).
|
|
22
|
+
|
|
7
23
|
## [1.5.0] — 2026-06-26
|
|
8
24
|
|
|
9
25
|
### Added
|
package/package.json
CHANGED
package/src/core/mount.js
CHANGED
|
@@ -11,18 +11,25 @@ import { createOwner, runWithOwner, disposeOwner } from "../reactivity/owner.js"
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {Function|object} component a component function, or an html() result
|
|
13
13
|
* @param {Element|string} target a DOM element or a CSS selector
|
|
14
|
+
* @param {{ hydrate?: boolean }} [options] with `hydrate: true`, adopt the
|
|
15
|
+
* server-rendered DOM already inside `target` instead of replacing it — the
|
|
16
|
+
* elements/attributes/events are reused in place (used by @zoijs/ssr's
|
|
17
|
+
* `hydrate()`). The returned unmount() disposes everything either way.
|
|
14
18
|
* @returns {Function} unmount
|
|
15
19
|
*/
|
|
16
|
-
export function mount(component, target) {
|
|
20
|
+
export function mount(component, target, options) {
|
|
17
21
|
const el = resolveTarget(target);
|
|
18
22
|
const owner = createOwner();
|
|
23
|
+
const hydrate = !!(options && options.hydrate);
|
|
19
24
|
|
|
20
25
|
let node;
|
|
21
26
|
runWithOwner(owner, () => {
|
|
22
27
|
const result = typeof component === "function" ? component() : component;
|
|
23
|
-
|
|
28
|
+
// Hydration binds to el's existing children in place; a fresh mount builds a
|
|
29
|
+
// detached fragment we then swap in.
|
|
30
|
+
node = render(result, hydrate ? el : undefined).node;
|
|
24
31
|
});
|
|
25
|
-
el.replaceChildren(node);
|
|
32
|
+
if (!hydrate) el.replaceChildren(node);
|
|
26
33
|
|
|
27
34
|
return () => {
|
|
28
35
|
disposeOwner(owner);
|
package/src/core/renderer.js
CHANGED
|
@@ -24,15 +24,21 @@ const noop = () => {};
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* @param {{ template: HTMLTemplateElement, parts: object[], values: any[] }} result
|
|
27
|
-
* @
|
|
27
|
+
* @param {Element} [hydrateRoot] when given, bind to this EXISTING server-rendered
|
|
28
|
+
* subtree in place (no clone) instead of building fresh DOM — see hydration below.
|
|
29
|
+
* @returns {{ node: Node, dispose: Function }}
|
|
28
30
|
*/
|
|
29
|
-
export function render(result) {
|
|
31
|
+
export function render(result, hydrateRoot) {
|
|
30
32
|
const owner = createOwner(); // nested under the active owner
|
|
31
|
-
|
|
33
|
+
// Hydration adopts the server DOM in place — elements, attributes, and events are
|
|
34
|
+
// reused, never re-created; each dynamic slot is cleared + re-rendered (same
|
|
35
|
+
// values → no visible change). Otherwise build a fresh clone.
|
|
36
|
+
const hydrating = hydrateRoot != null;
|
|
37
|
+
const fragment = hydrating ? hydrateRoot : result.template.content.cloneNode(true);
|
|
32
38
|
const { parts, values } = result;
|
|
33
39
|
|
|
34
|
-
// Collect every part's target node
|
|
35
|
-
//
|
|
40
|
+
// Collect every part's target node first (document order), so a binding that
|
|
41
|
+
// inserts children can't shadow a later part.
|
|
36
42
|
const nodes = collectNodes(fragment, parts, result.hasElements);
|
|
37
43
|
|
|
38
44
|
runWithOwner(owner, () => {
|
|
@@ -42,6 +48,7 @@ export function render(result) {
|
|
|
42
48
|
if (!node) continue;
|
|
43
49
|
|
|
44
50
|
if (part.type === "child") {
|
|
51
|
+
if (hydrating) clearHydratedSlot(node); // drop this slot's server content
|
|
45
52
|
bindChild(node, values[part.hole]);
|
|
46
53
|
} else {
|
|
47
54
|
node.removeAttribute("data-zoijs-bind");
|
|
@@ -53,6 +60,20 @@ export function render(result) {
|
|
|
53
60
|
return { node: fragment, dispose: () => disposeOwner(owner) };
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
// Remove a hydrated slot's server content: the nodes before its anchor, back
|
|
64
|
+
// through the slot's `<!--zoijs:[-->` start marker (emitted by @zoijs/ssr). Static
|
|
65
|
+
// text outside the slot is preserved; the normal binding then renders fresh content
|
|
66
|
+
// in the same place. No start marker → leave the DOM as-is (not a hydratable slot).
|
|
67
|
+
const isSlotStart = (n) => n && n.nodeType === 8 && n.data === "zoijs:[";
|
|
68
|
+
function clearHydratedSlot(anchor) {
|
|
69
|
+
let start = anchor.previousSibling;
|
|
70
|
+
while (start && !isSlotStart(start)) start = start.previousSibling;
|
|
71
|
+
if (!start) return;
|
|
72
|
+
let n;
|
|
73
|
+
while ((n = anchor.previousSibling) !== start) n.remove();
|
|
74
|
+
start.remove();
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
// Match parts to nodes by document order: a child part ↔ the next marker comment,
|
|
57
78
|
// an element part ↔ the next element carrying data-zoijs-bind. Unique markers make
|
|
58
79
|
// this collision-proof across nested templates and list items.
|
package/src/index.d.ts
CHANGED
|
@@ -66,11 +66,26 @@ export type Ref<E extends Element = Element> = (element: E) => void | (() => voi
|
|
|
66
66
|
*/
|
|
67
67
|
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
|
|
68
68
|
|
|
69
|
+
/** Options for {@link mount}. */
|
|
70
|
+
export interface MountOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Adopt the server-rendered DOM already inside `target` (from `@zoijs/ssr`'s
|
|
73
|
+
* `renderToString(..., { hydratable: true })`) instead of replacing it: elements,
|
|
74
|
+
* attributes, and events are reused in place. Used by `@zoijs/ssr`'s `hydrate()`.
|
|
75
|
+
*/
|
|
76
|
+
hydrate?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Render a component (or a template) into a DOM element or CSS selector.
|
|
71
81
|
* Returns an `unmount()` that detaches the DOM and disposes all reactivity.
|
|
82
|
+
* With `{ hydrate: true }`, binds to the existing server-rendered DOM in place.
|
|
72
83
|
*/
|
|
73
|
-
export function mount(
|
|
84
|
+
export function mount(
|
|
85
|
+
component: Component | TemplateResult,
|
|
86
|
+
target: Element | string,
|
|
87
|
+
options?: MountOptions
|
|
88
|
+
): () => void;
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
91
|
* Create a reactive value.
|