@vmz/core 0.3.13 → 0.3.15
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/browser/client-nav.types.d.ts +2 -5
- package/dist/browser/direct-api.types.d.ts +11 -0
- package/dist/browser/direct-host-box.d.ts +11 -0
- package/dist/browser/direct-host-box.js +13 -0
- package/dist/browser/dom/attributes.js +142 -2
- package/dist/browser/dom/direct-api.d.ts +51 -3
- package/dist/browser/dom/direct-api.js +489 -88
- package/dist/browser/dom-core.types.d.ts +19 -2
- package/dist/browser/navigation/client-navigation.js +18 -30
- package/dist/browser/reactivity/write-barrier.js +5 -1
- package/dist/browser/resume/adopt.js +284 -14
- package/dist/browser/resume/hydrate.js +34 -0
- package/dist/host/deployment/deployment-registry.js +5 -2
- package/dist/host/render/render-host.js +2 -0
- package/dist/host/serve/bootstrap.js +8 -4
- package/dist/host/serve/hmr-dev.d.ts +16 -17
- package/dist/host/serve/hmr-dev.js +96 -35
- package/dist/host/serve/http.js +25 -8
- package/dist/host/serve/pack-entry.d.ts +21 -0
- package/dist/host/serve/pack-entry.js +78 -7
- package/dist/host/serve/page-render.js +10 -8
- package/dist/shared/dom-attr-normalize.d.ts +6 -0
- package/dist/shared/dom-attr-normalize.js +1 -1
- package/dist/shared/host.types.d.ts +9 -0
- package/dist/shared/html-digest.d.ts +15 -0
- package/dist/shared/html-digest.js +25 -0
- package/dist/shared/row-key.d.ts +13 -0
- package/dist/shared/row-key.js +19 -0
- package/dist/ssr/serialize.js +357 -17
- package/package.json +7 -7
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { mergeClassParts, mergeStyleParts } from '../../shared/dom-attr-normalize.js';
|
|
2
|
-
import {
|
|
2
|
+
import { HTML_HASH_ATTR, htmlDigest } from '../../shared/html-digest.js';
|
|
3
|
+
import { applyDirectHostBox, resolveDirectHostTag } from '../direct-host-box.js';
|
|
3
4
|
import { createUnknownComponentElement } from '../unknown-component.js';
|
|
4
5
|
import { applyDomAttr, eventPropHandlerName } from './attributes.js';
|
|
5
|
-
import { createInstance, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
|
|
6
|
+
import { createInstance, destroy, disposeDomTree, findOwnedDefaultSlotTarget, findOwnedNamedSlot, insertProjectedSlotNode, parkSlotNode, runDirectCreate, slotAnchorFor, stripFns, } from './component.js';
|
|
6
7
|
import { isEventEntryStrategy, noteDomCreate, noteDomRemove, scheduleClientOn } from './lifecycle.js';
|
|
7
8
|
import { components, resolveComponent } from './registry.js';
|
|
8
9
|
import { beginEventFlush, endEventFlush, inferHandlerMethod, methodAllowsSyncEventFlush, registerBind, runDomEventHandler, runPatch, scheduleRefresh, unregisterBind, } from '../reactivity/patch-scheduler.js';
|
|
9
10
|
let directPropBindSeq = 0;
|
|
10
11
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
12
|
+
/** Vue accepts a function or an array of functions for one handler slot. */
|
|
13
|
+
function normalizeHandlerList(value) {
|
|
14
|
+
if (typeof value === 'function')
|
|
15
|
+
return [value];
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.filter((fn) => typeof fn === 'function');
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* Nesting depth of inline `<svg>` subtrees during create/resume.
|
|
13
22
|
*
|
|
@@ -30,19 +39,136 @@ function isSvgNode(node) {
|
|
|
30
39
|
const tag = node.tagName;
|
|
31
40
|
return tag != null && String(tag).toLowerCase() === 'svg' && ns == null;
|
|
32
41
|
}
|
|
42
|
+
/** Component display name for a ctor or registry key (`<component :is>` too). */
|
|
43
|
+
function componentDisplayName(nameOrCtor) {
|
|
44
|
+
if (typeof nameOrCtor === 'function') {
|
|
45
|
+
const fn = nameOrCtor;
|
|
46
|
+
return fn.__vmzTag || fn.name || 'Component';
|
|
47
|
+
}
|
|
48
|
+
return nameOrCtor == null ? 'Component' : String(nameOrCtor);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Bind props into `host`, resolve the target component, create its child
|
|
52
|
+
* instance and run its create. Shared by the static `component()` and the
|
|
53
|
+
* `dynamicComponent()` remount path so both produce the same boundary.
|
|
54
|
+
*/
|
|
55
|
+
function mountDirectComponentHost(host, hostInst, name, nameOrCtor, props, client) {
|
|
56
|
+
const rawProps = (props || {});
|
|
57
|
+
// `id` / `data-*` / `aria-*` are fallthrough attributes, not props: they
|
|
58
|
+
// belong on the host element (the component boundary in VMZ).
|
|
59
|
+
const hostAttrs = rawProps.__hostAttrs;
|
|
60
|
+
if (hostAttrs) {
|
|
61
|
+
for (const [k, v] of Object.entries(hostAttrs))
|
|
62
|
+
applyDomAttr(host, k, v);
|
|
63
|
+
}
|
|
64
|
+
const resolved = {};
|
|
65
|
+
for (const [k, v] of Object.entries(rawProps)) {
|
|
66
|
+
if (k === '__hostAttrs')
|
|
67
|
+
continue;
|
|
68
|
+
// Function props that already look like `onXxx` stay as handlers.
|
|
69
|
+
// Component `@event` never arrives here (emit uses onComponentEvent).
|
|
70
|
+
const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
|
|
71
|
+
if (onKey)
|
|
72
|
+
resolved[onKey] = v;
|
|
73
|
+
else if (typeof v === 'function')
|
|
74
|
+
resolved[k] = v.call(hostInst);
|
|
75
|
+
else
|
|
76
|
+
resolved[k] = v;
|
|
77
|
+
}
|
|
78
|
+
if (client) {
|
|
79
|
+
host.setAttribute('data-vmz-island', name);
|
|
80
|
+
host.setAttribute('data-vmz-client', String(client));
|
|
81
|
+
host.setAttribute('data-vmz-props', JSON.stringify(stripFns(resolved)));
|
|
82
|
+
if (isEventEntryStrategy(String(client))) {
|
|
83
|
+
host.setAttribute('data-vmz-entry', 'event');
|
|
84
|
+
}
|
|
85
|
+
// resume: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
|
|
86
|
+
scheduleClientOn(host, String(client), async () => {
|
|
87
|
+
const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : await resolveComponent(name);
|
|
88
|
+
if (!Ctor) {
|
|
89
|
+
// Replace placeholder island with leaf error node (do not throw page).
|
|
90
|
+
const err = createUnknownComponentElement(name, 'island');
|
|
91
|
+
host.replaceWith(err);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const { resume } = await import('../resume/resume.js');
|
|
95
|
+
await resume(Ctor, host, { props: resolved, state: {} });
|
|
96
|
+
});
|
|
97
|
+
return host;
|
|
98
|
+
}
|
|
99
|
+
// Static Ctor import path: use Function directly (no registry lookup).
|
|
100
|
+
const Ctor = (typeof nameOrCtor === 'function' ? nameOrCtor : components[name]);
|
|
101
|
+
if (!Ctor) {
|
|
102
|
+
return createUnknownComponentElement(name, 'client');
|
|
103
|
+
}
|
|
104
|
+
// Inline chips use `display: contents` (`ui-direct-host-box`). Block surfaces
|
|
105
|
+
// keep a real box (DataTable select timed out when defaulting everything).
|
|
106
|
+
// A dynamic switch must reset the previous box before applying the new one.
|
|
107
|
+
// Writing an empty declaration at all still materialized a `style` attribute
|
|
108
|
+
// on hosts that never had one, so hydrate rewrote the SSR DOM for block
|
|
109
|
+
// surfaces. Only inline chips (`display: contents`) have a box to reset.
|
|
110
|
+
if (host.style && host.style.display === 'contents')
|
|
111
|
+
host.style.removeProperty('display');
|
|
112
|
+
applyDirectHostBox(host, name, Ctor);
|
|
113
|
+
const child = createInstance(Ctor, resolved);
|
|
114
|
+
if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
|
|
115
|
+
throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
|
116
|
+
}
|
|
117
|
+
// Nested resume: keep parent `_resumeAdopt` so child reclaim parked SSR nodes.
|
|
118
|
+
// Enter the host's private child pool so nested create cannot see uncle siblings.
|
|
119
|
+
const adopt = directApi._resumeAdopt;
|
|
120
|
+
const entered = adopt && typeof adopt.enter === 'function' ? adopt.enter(host) : false;
|
|
121
|
+
// A child nested inside the parent's `v-for` must not feed the parent item
|
|
122
|
+
// patch sink: those patches belong to the child instance (`this`) and the
|
|
123
|
+
// parent's each replays them with the parent host, wiping child output.
|
|
124
|
+
const prevItemPatches = directApi._itemPatches;
|
|
125
|
+
directApi._itemPatches = null;
|
|
126
|
+
let node;
|
|
127
|
+
try {
|
|
128
|
+
node = runDirectCreate(Ctor, child);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
directApi._itemPatches = prevItemPatches;
|
|
132
|
+
if (entered && adopt && typeof adopt.leave === 'function')
|
|
133
|
+
adopt.leave();
|
|
134
|
+
}
|
|
135
|
+
if (node) {
|
|
136
|
+
// ifBlock/eachBlock roots are DocumentFragments: appendChild moves
|
|
137
|
+
// children into `host` and empties the fragment. Keep a live Element
|
|
138
|
+
// root so projectDefaultSlot can find `<slot>` (Button v-if/v-else).
|
|
139
|
+
child.__vmzDomRoot = node.nodeType === 11 ? host : node;
|
|
140
|
+
host.appendChild(node);
|
|
141
|
+
}
|
|
142
|
+
host.__vmzInst = child;
|
|
143
|
+
if (typeof child.onMount === 'function') {
|
|
144
|
+
const pending = Promise.resolve().then(() => {
|
|
145
|
+
if (!child.__vmzDestroyed)
|
|
146
|
+
return child.onMount();
|
|
147
|
+
});
|
|
148
|
+
const bag = hostInst.__vmzPendingChildMounts || (hostInst.__vmzPendingChildMounts = []);
|
|
149
|
+
bag.push(pending);
|
|
150
|
+
}
|
|
151
|
+
return host;
|
|
152
|
+
}
|
|
33
153
|
export const directApi = {
|
|
34
154
|
_inst: null,
|
|
35
155
|
_branchBinds: null,
|
|
36
156
|
_itemPatches: null,
|
|
37
157
|
_eachCtx: null,
|
|
38
158
|
_resumeAdopt: null,
|
|
39
|
-
el(tag) {
|
|
159
|
+
el(tag, key) {
|
|
40
160
|
const name = tag || 'div';
|
|
41
161
|
const svg = inSvgScope() || name === 'svg';
|
|
162
|
+
const rowKey = key == null ? null : key;
|
|
42
163
|
if (directApi._resumeAdopt)
|
|
43
|
-
return directApi._resumeAdopt.el(name, svg);
|
|
164
|
+
return directApi._resumeAdopt.el(name, svg, rowKey);
|
|
44
165
|
noteDomCreate();
|
|
45
|
-
|
|
166
|
+
const node = svg ? document.createElementNS(SVG_NS, name) : document.createElement(name);
|
|
167
|
+
// Row keys live as expandos on the live DOM (the clone kernel does the same)
|
|
168
|
+
// and as `data-vmz-key` in SSR output for resume reclamation.
|
|
169
|
+
if (rowKey != null)
|
|
170
|
+
node.__vmzKey = rowKey;
|
|
171
|
+
return node;
|
|
46
172
|
},
|
|
47
173
|
elNS(ns, tag) {
|
|
48
174
|
if (directApi._resumeAdopt && typeof directApi._resumeAdopt.elNS === 'function') {
|
|
@@ -57,6 +183,52 @@ export const directApi = {
|
|
|
57
183
|
noteDomCreate();
|
|
58
184
|
return document.createTextNode(value == null ? '' : String(value));
|
|
59
185
|
},
|
|
186
|
+
/**
|
|
187
|
+
* Emitted `v-html` write. Resume parks the SSR children of a reclaimed host,
|
|
188
|
+
* so the live `innerHTML` is still empty when the first patch runs. SSR stamps
|
|
189
|
+
* `HTML_HASH_ATTR` with the digest of the exact payload string it wrote: a
|
|
190
|
+
* matching digest proves the parked subtree is the same payload, so adopt
|
|
191
|
+
* moves it back and no text is ever re-parsed. A different digest writes the
|
|
192
|
+
* new payload and refreshes the marker for later identical runs.
|
|
193
|
+
*/
|
|
194
|
+
html(el, value) {
|
|
195
|
+
let next;
|
|
196
|
+
if (value == null) {
|
|
197
|
+
// `null` is "this create has no payload yet", not "the payload is
|
|
198
|
+
// empty": a resumed host may still own the SSR payload, so hand the
|
|
199
|
+
// parked subtree back instead of clearing the server's markup. After
|
|
200
|
+
// the resume pass ends `_resumeAdopt` is gone and a null patch
|
|
201
|
+
// clears the host like Vue's `v-html` does.
|
|
202
|
+
const adopt = directApi._resumeAdopt;
|
|
203
|
+
if (adopt && typeof adopt.keepHtml === 'function' && adopt.keepHtml(el))
|
|
204
|
+
return;
|
|
205
|
+
// Already holding the SSR payload from an earlier null create: the
|
|
206
|
+
// create and its tracked patch both pass null, and only a real payload
|
|
207
|
+
// (or a host without one) may release the server's markup.
|
|
208
|
+
if (el.__vmzHtmlKeep)
|
|
209
|
+
return;
|
|
210
|
+
next = '';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
el.__vmzHtmlKeep = false;
|
|
214
|
+
next = String(value);
|
|
215
|
+
}
|
|
216
|
+
const digest = htmlDigest(next);
|
|
217
|
+
const adopt = directApi._resumeAdopt;
|
|
218
|
+
if (adopt && typeof adopt.reclaimHtml === 'function' && adopt.reclaimHtml(el, digest))
|
|
219
|
+
return;
|
|
220
|
+
// The browser serializer expands self-closing tags (`<path/>` becomes
|
|
221
|
+
// `<path></path>`), so `innerHTML` never equals the generated string for
|
|
222
|
+
// SVG payloads. Compare the payload digest instead: an equal stamp means
|
|
223
|
+
// this host already holds exactly this payload, so a repeated patch must
|
|
224
|
+
// keep the live nodes (identity, focus, and `ResizeObserver` targets).
|
|
225
|
+
if (typeof el.getAttribute === 'function' && el.getAttribute(HTML_HASH_ATTR) === digest)
|
|
226
|
+
return;
|
|
227
|
+
if (el.innerHTML !== next)
|
|
228
|
+
el.innerHTML = next;
|
|
229
|
+
if (typeof el.setAttribute === 'function')
|
|
230
|
+
el.setAttribute(HTML_HASH_ATTR, digest);
|
|
231
|
+
},
|
|
60
232
|
nsScopeDepth() {
|
|
61
233
|
return nsStack.length;
|
|
62
234
|
},
|
|
@@ -76,6 +248,17 @@ export const directApi = {
|
|
|
76
248
|
}
|
|
77
249
|
return false;
|
|
78
250
|
},
|
|
251
|
+
adoptEnterSlot(hostEl, name) {
|
|
252
|
+
nsStack.push(isSvgNode(hostEl));
|
|
253
|
+
const adopt = directApi._resumeAdopt;
|
|
254
|
+
if (adopt && typeof adopt.enterSlot === 'function') {
|
|
255
|
+
const ok = adopt.enterSlot(hostEl, name);
|
|
256
|
+
if (ok)
|
|
257
|
+
adopt._enterBalance = (adopt._enterBalance || 0) + 1;
|
|
258
|
+
return ok;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
},
|
|
79
262
|
adoptLeave() {
|
|
80
263
|
nsStack.pop();
|
|
81
264
|
const adopt = directApi._resumeAdopt;
|
|
@@ -93,6 +276,22 @@ export const directApi = {
|
|
|
93
276
|
noteDomCreate();
|
|
94
277
|
return document.createDocumentFragment();
|
|
95
278
|
},
|
|
279
|
+
/**
|
|
280
|
+
* Multi-root `<template v-for>` row. Resume narrows adoption to the SSR
|
|
281
|
+
* marker pair for this key; a key with no pair gets an empty scope so a new
|
|
282
|
+
* row can never steal nodes that belong to another key.
|
|
283
|
+
*/
|
|
284
|
+
fragRow(key) {
|
|
285
|
+
const adopt = directApi._resumeAdopt;
|
|
286
|
+
if (adopt && typeof adopt.rowStart === 'function')
|
|
287
|
+
adopt.rowStart(key);
|
|
288
|
+
return directApi.frag();
|
|
289
|
+
},
|
|
290
|
+
rowEnd() {
|
|
291
|
+
const adopt = directApi._resumeAdopt;
|
|
292
|
+
if (adopt && typeof adopt.rowEnd === 'function')
|
|
293
|
+
adopt.rowEnd();
|
|
294
|
+
},
|
|
96
295
|
comment(value) {
|
|
97
296
|
noteDomCreate();
|
|
98
297
|
return document.createComment(value == null ? '' : String(value));
|
|
@@ -161,6 +360,15 @@ export const directApi = {
|
|
|
161
360
|
disposeDomTree(root);
|
|
162
361
|
},
|
|
163
362
|
attr(el, name, value) {
|
|
363
|
+
// `v-pre`: the authored pair goes out untouched — no `className` → `class`
|
|
364
|
+
// rewrite, no class/style merge, no property special-casing.
|
|
365
|
+
if (el.__vmzVerbatim) {
|
|
366
|
+
if (value == null || value === false)
|
|
367
|
+
el.removeAttribute(name);
|
|
368
|
+
else
|
|
369
|
+
el.setAttribute(name, value === true ? '' : String(value));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
164
372
|
applyDomAttr(el, name, value);
|
|
165
373
|
},
|
|
166
374
|
mergeClass(...parts) {
|
|
@@ -335,9 +543,11 @@ export const directApi = {
|
|
|
335
543
|
raw = get.call(inst);
|
|
336
544
|
}
|
|
337
545
|
catch {
|
|
338
|
-
raw =
|
|
546
|
+
raw = null;
|
|
339
547
|
}
|
|
340
|
-
|
|
548
|
+
// Same payload contract as the emitted `v-html` call: digest
|
|
549
|
+
// matching and resume adoption must not be bypassed here.
|
|
550
|
+
directApi.html(el, raw);
|
|
341
551
|
}, bindingId);
|
|
342
552
|
},
|
|
343
553
|
/**
|
|
@@ -359,96 +569,87 @@ export const directApi = {
|
|
|
359
569
|
/**
|
|
360
570
|
* Nested component (sync Direct child or island schedule).
|
|
361
571
|
*/
|
|
362
|
-
component(hostInst, nameOrCtor, props, client) {
|
|
363
|
-
const name =
|
|
572
|
+
component(hostInst, nameOrCtor, props, client, key) {
|
|
573
|
+
const name = componentDisplayName(nameOrCtor);
|
|
364
574
|
let host = null;
|
|
365
575
|
if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
|
|
366
|
-
host = directApi._resumeAdopt.componentHost(name);
|
|
576
|
+
host = directApi._resumeAdopt.componentHost(name, key);
|
|
367
577
|
}
|
|
368
578
|
if (!host) {
|
|
369
579
|
noteDomCreate();
|
|
370
|
-
host = document.createElement(
|
|
580
|
+
host = document.createElement(resolveDirectHostTag(name, nameOrCtor));
|
|
371
581
|
host.setAttribute('data-vmz', name);
|
|
372
582
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const err = createUnknownComponentElement(name, 'island');
|
|
398
|
-
host.replaceWith(err);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const { resume } = await import('../resume/resume.js');
|
|
402
|
-
await resume(Ctor, host, { props: resolved, state: {} });
|
|
403
|
-
});
|
|
404
|
-
return host;
|
|
405
|
-
}
|
|
406
|
-
// Static Ctor import path: use Function directly (no registry lookup).
|
|
407
|
-
const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : components[name];
|
|
408
|
-
if (!Ctor) {
|
|
409
|
-
return createUnknownComponentElement(name, 'client');
|
|
410
|
-
}
|
|
411
|
-
// Inline chips ??`display: contents` (`ui-direct-host-box`). Block surfaces
|
|
412
|
-
// keep a real box (DataTable select timed out when defaulting everything).
|
|
413
|
-
applyDirectHostBox(host, name, Ctor);
|
|
414
|
-
const child = createInstance(Ctor, resolved);
|
|
415
|
-
if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
|
|
416
|
-
throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
|
417
|
-
}
|
|
418
|
-
// Nested resume: keep parent `_resumeAdopt` so child reclaim parked SSR nodes.
|
|
419
|
-
// Enter the host's private child pool so nested create cannot see uncle siblings.
|
|
420
|
-
const adopt = directApi._resumeAdopt;
|
|
421
|
-
const entered = adopt && typeof adopt.enter === 'function' ? adopt.enter(host) : false;
|
|
422
|
-
// A child nested inside the parent's `v-for` must not feed the parent item
|
|
423
|
-
// patch sink: those patches belong to the child instance (`this`) and the
|
|
424
|
-
// parent's each replays them with the parent host, wiping child output.
|
|
425
|
-
const prevItemPatches = directApi._itemPatches;
|
|
426
|
-
directApi._itemPatches = null;
|
|
427
|
-
let node;
|
|
428
|
-
try {
|
|
429
|
-
node = runDirectCreate(Ctor, child);
|
|
430
|
-
}
|
|
431
|
-
finally {
|
|
432
|
-
directApi._itemPatches = prevItemPatches;
|
|
433
|
-
if (entered && adopt && typeof adopt.leave === 'function')
|
|
434
|
-
adopt.leave();
|
|
435
|
-
}
|
|
436
|
-
if (node) {
|
|
437
|
-
// ifBlock/eachBlock roots are DocumentFragments: appendChild moves
|
|
438
|
-
// children into `host` and empties the fragment. Keep a live Element
|
|
439
|
-
// root so projectDefaultSlot can find `<slot>` (Button v-if/v-else).
|
|
440
|
-
child.__vmzDomRoot = node.nodeType === 11 ? host : node;
|
|
441
|
-
host.appendChild(node);
|
|
583
|
+
if (key != null)
|
|
584
|
+
host.__vmzKey = key;
|
|
585
|
+
return mountDirectComponentHost(host, hostInst, name, nameOrCtor, props, client);
|
|
586
|
+
},
|
|
587
|
+
/**
|
|
588
|
+
* `<component :is>` boundary. The host element is stable, the child instance
|
|
589
|
+
* is torn down and rebuilt whenever the evaluated `is` value changes. Props
|
|
590
|
+
* are re-evaluated per mount so the new type starts from current values, and
|
|
591
|
+
* the child listener bag is carried over so `@event` handlers keep working.
|
|
592
|
+
*/
|
|
593
|
+
dynamicComponent(hostInst, bindingId, deps, getCtor, getProps, key) {
|
|
594
|
+
const resolve = () => {
|
|
595
|
+
try {
|
|
596
|
+
return getCtor.call(hostInst);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
const initial = resolve();
|
|
603
|
+
const name = componentDisplayName(initial);
|
|
604
|
+
let host = null;
|
|
605
|
+
if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
|
|
606
|
+
host = directApi._resumeAdopt.componentHost(name, key);
|
|
442
607
|
}
|
|
443
|
-
host
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return child.onMount();
|
|
448
|
-
});
|
|
449
|
-
const bag = hostInst.__vmzPendingChildMounts || (hostInst.__vmzPendingChildMounts = []);
|
|
450
|
-
bag.push(pending);
|
|
608
|
+
if (!host) {
|
|
609
|
+
noteDomCreate();
|
|
610
|
+
host = document.createElement(resolveDirectHostTag(name, initial));
|
|
611
|
+
host.setAttribute('data-vmz', name);
|
|
451
612
|
}
|
|
613
|
+
if (key != null)
|
|
614
|
+
host.__vmzKey = key;
|
|
615
|
+
let current = initial;
|
|
616
|
+
const render = (next) => {
|
|
617
|
+
const old = host.__vmzInst;
|
|
618
|
+
let listeners = null;
|
|
619
|
+
if (old) {
|
|
620
|
+
listeners = old.__vmzComponentListeners || null;
|
|
621
|
+
destroy(old);
|
|
622
|
+
host.__vmzInst = null;
|
|
623
|
+
}
|
|
624
|
+
while (host.firstChild)
|
|
625
|
+
host.removeChild(host.firstChild);
|
|
626
|
+
current = next;
|
|
627
|
+
const nextName = componentDisplayName(next);
|
|
628
|
+
host.setAttribute('data-vmz', nextName);
|
|
629
|
+
if (!next)
|
|
630
|
+
return;
|
|
631
|
+
const Ctor = typeof next === 'function' ? next : components[nextName];
|
|
632
|
+
if (!Ctor)
|
|
633
|
+
return;
|
|
634
|
+
let props = {};
|
|
635
|
+
try {
|
|
636
|
+
props = getProps.call(hostInst) || {};
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
props = {};
|
|
640
|
+
}
|
|
641
|
+
mountDirectComponentHost(host, hostInst, nextName, Ctor, props, null);
|
|
642
|
+
const child = host.__vmzInst;
|
|
643
|
+
if (listeners && child)
|
|
644
|
+
child.__vmzComponentListeners = listeners;
|
|
645
|
+
};
|
|
646
|
+
render(initial);
|
|
647
|
+
directApi.trackPatch(hostInst, deps || [], function __vmzDynamicComponentPatch() {
|
|
648
|
+
const next = resolve();
|
|
649
|
+
if (next === current)
|
|
650
|
+
return;
|
|
651
|
+
render(next);
|
|
652
|
+
}, bindingId);
|
|
452
653
|
return host;
|
|
453
654
|
},
|
|
454
655
|
/**
|
|
@@ -499,6 +700,206 @@ export const directApi = {
|
|
|
499
700
|
scheduleRefresh(child, { type: 'replace', root: propName });
|
|
500
701
|
}, bindingId);
|
|
501
702
|
},
|
|
703
|
+
/**
|
|
704
|
+
* Object form of `bindComponentProp`: spread every key of the patch-time object
|
|
705
|
+
* across the child props and clear props whose keys disappear from the object.
|
|
706
|
+
*/
|
|
707
|
+
bindComponentProps(hostInst, hostEl, slot, deps, get) {
|
|
708
|
+
if (hostEl && hostEl.__vmzPropBindSeq == null) {
|
|
709
|
+
hostEl.__vmzPropBindSeq = ++directPropBindSeq;
|
|
710
|
+
}
|
|
711
|
+
const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
|
|
712
|
+
const bindingId = `pc:${seq}:*${slot || 'obj'}`;
|
|
713
|
+
const seen = Object.create(null);
|
|
714
|
+
directApi.trackPatch(hostInst, deps || [], function bindComponentPropsPatch() {
|
|
715
|
+
let raw;
|
|
716
|
+
try {
|
|
717
|
+
raw = get.call(hostInst);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
raw = null;
|
|
721
|
+
}
|
|
722
|
+
const child = hostEl && hostEl.__vmzInst;
|
|
723
|
+
if (!child || child.__vmzDestroyed)
|
|
724
|
+
return;
|
|
725
|
+
const next = Object.create(null);
|
|
726
|
+
if (raw && typeof raw === 'object') {
|
|
727
|
+
for (const key of Object.keys(raw)) {
|
|
728
|
+
if (!key || key.startsWith('#'))
|
|
729
|
+
continue;
|
|
730
|
+
next[key] = raw[key];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const notify = (key, value) => {
|
|
734
|
+
child[key] = value;
|
|
735
|
+
if (typeof child.__vmzOnParentProp === 'function') {
|
|
736
|
+
try {
|
|
737
|
+
child.__vmzOnParentProp(key, value);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
console.error('vmz:dom __vmzOnParentProp', err);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
scheduleRefresh(child, { type: 'replace', root: key });
|
|
744
|
+
};
|
|
745
|
+
for (const key of Object.keys(seen)) {
|
|
746
|
+
if (key in next)
|
|
747
|
+
continue;
|
|
748
|
+
delete seen[key];
|
|
749
|
+
notify(key, undefined);
|
|
750
|
+
}
|
|
751
|
+
for (const key of Object.keys(next)) {
|
|
752
|
+
const value = next[key];
|
|
753
|
+
const had = key in seen;
|
|
754
|
+
const prev = seen[key];
|
|
755
|
+
seen[key] = value;
|
|
756
|
+
// Create-time `...api.spreadProps()` already delivered identical
|
|
757
|
+
// values, so an unchanged first pass must not schedule a refresh.
|
|
758
|
+
if (had && Object.is(prev, value))
|
|
759
|
+
continue;
|
|
760
|
+
if (!had && Object.is(child[key], value))
|
|
761
|
+
continue;
|
|
762
|
+
notify(key, value);
|
|
763
|
+
}
|
|
764
|
+
}, bindingId);
|
|
765
|
+
},
|
|
766
|
+
/**
|
|
767
|
+
* Object form of `onComponentEvent`: diff event names per call site and detach
|
|
768
|
+
* listeners whose keys disappear. No global `off` semantics — the bag entry
|
|
769
|
+
* belongs to this call site only.
|
|
770
|
+
*/
|
|
771
|
+
bindComponentEvents(hostInst, hostEl, slot, deps, get) {
|
|
772
|
+
if (hostEl && hostEl.__vmzPropBindSeq == null) {
|
|
773
|
+
hostEl.__vmzPropBindSeq = ++directPropBindSeq;
|
|
774
|
+
}
|
|
775
|
+
const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
|
|
776
|
+
const attached = Object.create(null);
|
|
777
|
+
directApi.trackPatch(hostInst, deps || [], function bindComponentEventsPatch() {
|
|
778
|
+
let raw;
|
|
779
|
+
try {
|
|
780
|
+
raw = get.call(hostInst);
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
raw = null;
|
|
784
|
+
}
|
|
785
|
+
const child = hostEl && hostEl.__vmzInst;
|
|
786
|
+
if (!child || child.__vmzDestroyed)
|
|
787
|
+
return;
|
|
788
|
+
const next = Object.create(null);
|
|
789
|
+
if (raw && typeof raw === 'object') {
|
|
790
|
+
for (const key of Object.keys(raw)) {
|
|
791
|
+
if (!key || key.startsWith('#'))
|
|
792
|
+
continue;
|
|
793
|
+
const list = normalizeHandlerList(raw[key]);
|
|
794
|
+
if (list.length)
|
|
795
|
+
next[key] = list;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const bag = child.__vmzComponentListeners ||
|
|
799
|
+
(child.__vmzComponentListeners = Object.create(null));
|
|
800
|
+
for (const key of Object.keys(attached)) {
|
|
801
|
+
if (key in next)
|
|
802
|
+
continue;
|
|
803
|
+
const rec = attached[key];
|
|
804
|
+
delete attached[key];
|
|
805
|
+
const list = bag[key];
|
|
806
|
+
if (!Array.isArray(list))
|
|
807
|
+
continue;
|
|
808
|
+
const index = list.indexOf(rec.wrapper);
|
|
809
|
+
if (index >= 0)
|
|
810
|
+
list.splice(index, 1);
|
|
811
|
+
if (!list.length)
|
|
812
|
+
delete bag[key];
|
|
813
|
+
}
|
|
814
|
+
for (const key of Object.keys(next)) {
|
|
815
|
+
const handlers = next[key];
|
|
816
|
+
const rec = attached[key];
|
|
817
|
+
if (rec) {
|
|
818
|
+
rec.handlers = handlers;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const created = { handlers, wrapper: null };
|
|
822
|
+
created.wrapper = (...args) => {
|
|
823
|
+
for (const fn of created.handlers)
|
|
824
|
+
fn(...args);
|
|
825
|
+
};
|
|
826
|
+
(bag[key] || (bag[key] = [])).push(created.wrapper);
|
|
827
|
+
attached[key] = created;
|
|
828
|
+
}
|
|
829
|
+
}, `pcev:${seq}:${slot || 'obj'}`);
|
|
830
|
+
},
|
|
831
|
+
/**
|
|
832
|
+
* `v-bind="obj"` / `v-on="obj"` listeners scoped to one call site.
|
|
833
|
+
*
|
|
834
|
+
* `mode: 'bind'` reads `onXxx` keys out of a bind object, `mode: 'on'` reads
|
|
835
|
+
* event names directly. Vue lowercases element listener names, so both modes
|
|
836
|
+
* normalize before touching `addEventListener`.
|
|
837
|
+
*/
|
|
838
|
+
bindObjectListeners(el, slot, source, inst, mode) {
|
|
839
|
+
if (!el || !slot)
|
|
840
|
+
return;
|
|
841
|
+
const bag = el.__vmzObjEvt || (el.__vmzObjEvt = Object.create(null));
|
|
842
|
+
const state = bag[slot] || (bag[slot] = { types: Object.create(null) });
|
|
843
|
+
const prev = state.types;
|
|
844
|
+
const next = Object.create(null);
|
|
845
|
+
if (source && typeof source === 'object') {
|
|
846
|
+
for (const key of Object.keys(source)) {
|
|
847
|
+
let type;
|
|
848
|
+
if (mode === 'bind') {
|
|
849
|
+
if (!/^on[A-Z]/.test(key))
|
|
850
|
+
continue;
|
|
851
|
+
type = key.slice(2).toLowerCase();
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
type = String(key).toLowerCase();
|
|
855
|
+
}
|
|
856
|
+
const list = normalizeHandlerList(source[key]);
|
|
857
|
+
if (type && list.length)
|
|
858
|
+
next[type] = list;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
for (const type of Object.keys(prev)) {
|
|
862
|
+
if (type in next)
|
|
863
|
+
continue;
|
|
864
|
+
el.removeEventListener(type, prev[type].wrapper);
|
|
865
|
+
delete prev[type];
|
|
866
|
+
}
|
|
867
|
+
for (const type of Object.keys(next)) {
|
|
868
|
+
const handlers = next[type];
|
|
869
|
+
const rec = prev[type];
|
|
870
|
+
if (rec) {
|
|
871
|
+
rec.handlers = handlers;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const created = { handlers, wrapper: null };
|
|
875
|
+
created.wrapper = (ev) => {
|
|
876
|
+
if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
|
|
877
|
+
ev.preventDefault();
|
|
878
|
+
}
|
|
879
|
+
runDomEventHandler(inst || null, null, () => {
|
|
880
|
+
for (const fn of created.handlers)
|
|
881
|
+
fn.call(inst || null, ev);
|
|
882
|
+
});
|
|
883
|
+
};
|
|
884
|
+
prev[type] = created;
|
|
885
|
+
el.addEventListener(type, created.wrapper);
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
/**
|
|
889
|
+
* Filter a `v-bind="obj"` source down to bindable keys before it reaches
|
|
890
|
+
* component props (`#slot` markers belong to the compiler, not the child).
|
|
891
|
+
*/
|
|
892
|
+
spreadProps(source) {
|
|
893
|
+
const out = Object.create(null);
|
|
894
|
+
if (source && typeof source === 'object') {
|
|
895
|
+
for (const key of Object.keys(source)) {
|
|
896
|
+
if (!key || key.startsWith('#'))
|
|
897
|
+
continue;
|
|
898
|
+
out[key] = source[key];
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
},
|
|
502
903
|
/**
|
|
503
904
|
* Project parent children into nested Direct component default `<slot>`.
|
|
504
905
|
* Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
|