@ape-egg/vibe 4.0.1 → 4.1.1
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/README.md +3 -1
- package/boot.js +10 -9
- package/component.js +0 -29
- package/hot-module-refresh.js +0 -0
- package/index.js +0 -28
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +0 -56
- package/runtime/affected.js +6 -189
- package/runtime/cleanup.js +4 -35
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +1 -406
- package/runtime/conditionals.js +1 -143
- package/runtime/constants.js +19 -82
- package/runtime/debug.js +22 -47
- package/runtime/dispatch.js +0 -46
- package/runtime/hydrate.js +10 -129
- package/runtime/index.js +7 -365
- package/runtime/iterate.js +7 -595
- package/runtime/iteration-utils.js +18 -71
- package/runtime/loop-scope.js +0 -58
- package/runtime/manifest.js +0 -27
- package/runtime/parse.js +2 -116
- package/runtime/pre-compiled-iterations.js +3 -51
- package/runtime/pre-compiled-manifest.js +6 -169
- package/runtime/raw-html.js +0 -5
- package/runtime/reconcile.js +4 -159
- package/runtime/staging.js +0 -57
- package/runtime/state.js +0 -61
- package/runtime/this-scope.js +0 -17
- package/runtime/tracking.js +0 -65
- package/runtime/utils.js +1 -144
- package/runtime/vibe-css.js +54 -0
- package/spa.js +0 -76
- package/vibe.css +22 -44
package/runtime/parse.js
CHANGED
|
@@ -18,17 +18,12 @@ import {
|
|
|
18
18
|
import { rewriteHandlerAliases } from './loop-scope.js';
|
|
19
19
|
import './this-scope.js';
|
|
20
20
|
|
|
21
|
-
// Walks up the DOM for the nearest component wrapper tagged by component.js.
|
|
22
|
-
// Used to rewrite `this.property` in event handlers to the component's state path.
|
|
23
21
|
const findComponentIdForElement = (element) => {
|
|
24
22
|
if (!element?.closest) return null;
|
|
25
23
|
const wrapper = element.closest('[data-vibe-component-id]');
|
|
26
24
|
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
27
25
|
};
|
|
28
26
|
|
|
29
|
-
// `key` is reserved on fetched-component wrappers and remounts on CHANGE — a
|
|
30
|
-
// literal key can never change, so it can never fire. Warn once per value
|
|
31
|
-
// instead of dying silently (reparses would otherwise spam).
|
|
32
27
|
const warnedStaticKeys = new Set();
|
|
33
28
|
const warnStaticKey = (key) => {
|
|
34
29
|
if (warnedStaticKeys.has(key)) return;
|
|
@@ -38,18 +33,6 @@ const warnStaticKey = (key) => {
|
|
|
38
33
|
);
|
|
39
34
|
};
|
|
40
35
|
|
|
41
|
-
// Single source of truth for reading attribute/name bindings off an element.
|
|
42
|
-
// Called from both the root handler and recursive() so they can't drift. Any
|
|
43
|
-
// element classified as a fetched component (`<component src>` or
|
|
44
|
-
// `<div class="component" src>`) captures ONLY a bound src (`src="@[page.src]"`
|
|
45
|
-
// — resolved by hydration before the fetch, re-mounted on change) and a bound
|
|
46
|
-
// key (`key="@[page.path]"` — a key change remounts the same src); every other
|
|
47
|
-
// attribute is a prop owned by processComponent and must stay raw — hydrating
|
|
48
|
-
// them would coerce objects to "[object Object]" or strip boolean-like attrs
|
|
49
|
-
// to empty. A mounted wrapper carries the authored bindings in data-vibe-src /
|
|
50
|
-
// data-vibe-key (stamped by finalize — the src attribute was consumed by the
|
|
51
|
-
// fetch), so the knowledge survives every wrapper replacement: reparsing the
|
|
52
|
-
// live DOM alone rebuilds it.
|
|
53
36
|
const captureAttributeBindings = (element, aliasSet) => {
|
|
54
37
|
const nodeName = element.nodeName;
|
|
55
38
|
const isFetchedComponent =
|
|
@@ -84,17 +67,11 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
84
67
|
const attr = element.attributes[j];
|
|
85
68
|
BINDING_REGEX.lastIndex = 0;
|
|
86
69
|
|
|
87
|
-
// A name-binding the compiler relocated into data-vibe-namebind because its
|
|
88
|
-
// expression has whitespace (barred from attribute-name position): the value
|
|
89
|
-
// holds the verbatim @[expr](s). Treat it as a name binding, not a value binding.
|
|
90
70
|
if (attr.name === 'data-vibe-namebind') {
|
|
91
71
|
nameBindings.push(attr.value);
|
|
92
72
|
continue;
|
|
93
73
|
}
|
|
94
74
|
|
|
95
|
-
// A src-family binding parked on data-vibe-<attr> (by an earlier parse of
|
|
96
|
-
// this template, or by component finalize neutralizing fetched HTML):
|
|
97
|
-
// bind it to the real attribute — hydration writes the evaluated URL there.
|
|
98
75
|
if (isFetchableElement && attr.name.startsWith('data-vibe-')) {
|
|
99
76
|
const realName = attr.name.slice(10);
|
|
100
77
|
if (FETCH_SRC_ATTRS.includes(realName)) {
|
|
@@ -103,11 +80,6 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
103
80
|
}
|
|
104
81
|
}
|
|
105
82
|
|
|
106
|
-
// A raw binding-valued src reaching a fetchable element fires a request
|
|
107
|
-
// for the literal `@[...]` text (innerHTML parse and cloneNode both
|
|
108
|
-
// trigger it). Park it on data-vibe-<attr> — after the loop, since
|
|
109
|
-
// removeAttribute would shift the live NamedNodeMap being iterated —
|
|
110
|
-
// so the template and every clone of it are inert until hydration.
|
|
111
83
|
if (isFetchableElement && FETCH_SRC_ATTRS.includes(attr.name)) {
|
|
112
84
|
BINDING_REGEX.lastIndex = 0;
|
|
113
85
|
if (BINDING_REGEX.test(attr.value)) {
|
|
@@ -117,32 +89,16 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
117
89
|
}
|
|
118
90
|
}
|
|
119
91
|
|
|
120
|
-
// Attribute name itself contains a binding (e.g. <icon @[section.icon]>).
|
|
121
92
|
if (BINDING_REGEX.test(attr.name)) {
|
|
122
93
|
nameBindings.push(attr.name);
|
|
123
94
|
continue;
|
|
124
95
|
}
|
|
125
96
|
|
|
126
|
-
// Event handlers get two compile-time rewrites, computed off the original
|
|
127
|
-
// value and written once:
|
|
128
|
-
// 1. `this.` inside a component scope → `$this(this).` — resolved at
|
|
129
|
-
// FIRE time by this-scope.js: declared state keys hit the bucket,
|
|
130
|
-
// everything else stays the native element. Fire-time resolution is
|
|
131
|
-
// load-bearing: compiled pages register component state after the
|
|
132
|
-
// initial parse, so no key check is possible here.
|
|
133
|
-
// 2. bare loop-variable aliases → `$scope(this,'alias')` (loop-scoped
|
|
134
|
-
// handlers — only when an enclosing <!-- each --> alias is in scope).
|
|
135
97
|
if (attr.name.startsWith('on')) {
|
|
136
98
|
let v = attr.value;
|
|
137
99
|
if (v.includes('this.')) {
|
|
138
100
|
const componentId = findComponentIdForElement(element);
|
|
139
101
|
if (componentId) {
|
|
140
|
-
// `$.this.X` (component-state write through the root) must be
|
|
141
|
-
// consumed as one reference BEFORE the bare `this.X` pass — that
|
|
142
|
-
// pass alone would leave the `$.` prefix behind and produce
|
|
143
|
-
// `$.$this(this).X`. Runtime-fetched components arrive with this
|
|
144
|
-
// form already rewritten by component.js; compiled pages inline the
|
|
145
|
-
// authored form, so parse meets it raw.
|
|
146
102
|
v = v.replace(STATE_THIS_PROP_REGEX, (_, prop) => `$['${componentId}'].${prop}`);
|
|
147
103
|
v = v.replace(THIS_PROP_REGEX, (_, prop) => `$this(this).${prop}`);
|
|
148
104
|
}
|
|
@@ -172,10 +128,6 @@ const captureAttributeBindings = (element, aliasSet) => {
|
|
|
172
128
|
};
|
|
173
129
|
};
|
|
174
130
|
|
|
175
|
-
// Walk a parsed children map for any iteration node carrying scoped handlers.
|
|
176
|
-
// Each iteration node's own flag already aggregates its descendants, so we take
|
|
177
|
-
// the flag without re-descending into its template; we still recurse through
|
|
178
|
-
// elements and conditional branches to reach nested iteration nodes.
|
|
179
131
|
const subtreeHasScopedHandlers = (nodes) => {
|
|
180
132
|
for (const key in nodes) {
|
|
181
133
|
const node = nodes[key];
|
|
@@ -211,30 +163,21 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
211
163
|
let result = {};
|
|
212
164
|
|
|
213
165
|
for (let i = 0; i < children.length; i++) {
|
|
214
|
-
// Skip if this index was part of an iteration template
|
|
215
166
|
if (skipIndices.has(i)) continue;
|
|
216
167
|
|
|
217
168
|
const element = children[i];
|
|
218
169
|
const { nodeName, childNodes, outerHTML, innerHTML, textContent } = element;
|
|
219
170
|
|
|
220
|
-
// Skip non-reactive elements and dehydrated elements
|
|
221
171
|
if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
|
|
222
172
|
if (element.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) || element.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
223
173
|
stats.skipped++;
|
|
224
174
|
continue;
|
|
225
175
|
}
|
|
226
176
|
|
|
227
|
-
// Handle iteration comments
|
|
228
177
|
if (nodeName === '#comment') {
|
|
229
178
|
const trimmedComment = textContent.trim();
|
|
230
179
|
const iterationMatch = parseIterationHeader(trimmedComment);
|
|
231
180
|
|
|
232
|
-
// An each-shaped comment that fails the grammar, WITH a matching /each,
|
|
233
|
-
// is an authored directive with a syntax error — code, not prose. It
|
|
234
|
-
// used to render its body once, silently: the framework's worst
|
|
235
|
-
// footgun. Directives scream like a SyntaxError in every mode. (A
|
|
236
|
-
// prose comment merely starting with "each", no /each below, stays an
|
|
237
|
-
// inert comment.)
|
|
238
181
|
if (
|
|
239
182
|
!iterationMatch &&
|
|
240
183
|
ITERATION_START_REGEX.test(trimmedComment) &&
|
|
@@ -246,8 +189,6 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
246
189
|
);
|
|
247
190
|
}
|
|
248
191
|
|
|
249
|
-
// A bare <!-- if --> / <!-- else if --> (no expression) with a
|
|
250
|
-
// matching /if ahead is the same class of authored error.
|
|
251
192
|
if (/^if$/.test(trimmedComment) || /^else\s+if$/.test(trimmedComment)) {
|
|
252
193
|
try {
|
|
253
194
|
findConditionalEnd(children, i + 1);
|
|
@@ -256,7 +197,6 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
256
197
|
'Expected <!-- if condition --> ... <!-- else if condition --> ... <!-- /if -->',
|
|
257
198
|
);
|
|
258
199
|
} catch {
|
|
259
|
-
// No /if below — an inert comment.
|
|
260
200
|
}
|
|
261
201
|
}
|
|
262
202
|
|
|
@@ -264,49 +204,27 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
264
204
|
const { arrayPath, itemAlias, keyExpr, indexAlias } = iterationMatch;
|
|
265
205
|
|
|
266
206
|
try {
|
|
267
|
-
// Find matching end comment
|
|
268
207
|
const endIndex = findEndComment(children, i + 1);
|
|
269
208
|
|
|
270
|
-
// Extract template nodes (between start and end comments)
|
|
271
209
|
const templateNodes = Array.from(children).slice(i + 1, endIndex);
|
|
272
210
|
|
|
273
|
-
// Create a temporary container for the template
|
|
274
211
|
const templateContainer = document.createElement('div');
|
|
275
212
|
templateNodes.forEach((node) => {
|
|
276
213
|
templateContainer.appendChild(node.cloneNode(true));
|
|
277
214
|
});
|
|
278
215
|
|
|
279
|
-
// Aliases in scope inside this loop's template = enclosing aliases plus
|
|
280
|
-
// this loop's item alias and (only when explicitly declared) its index
|
|
281
|
-
// alias. The implicit default index name is never auto-rewritten.
|
|
282
216
|
const childAliases = new Set(aliasSet);
|
|
283
217
|
childAliases.add(itemAlias);
|
|
284
218
|
if (indexAlias) childAliases.add(indexAlias);
|
|
285
219
|
|
|
286
|
-
// Parse the template recursively
|
|
287
220
|
const templateParsed = recursive([...templateContainer.childNodes], undefined, new Set(), stats, childAliases);
|
|
288
221
|
|
|
289
|
-
// A handler rewritten to `$scope(this,'alias')` only exists in this
|
|
290
|
-
// freshly-parsed runtime template — the manifest's compiled batchFn was
|
|
291
|
-
// generated from the original (unrewritten) template, so it must be
|
|
292
|
-
// bypassed for this iteration (see canUseCompiled). True when a direct
|
|
293
|
-
// handler in this template was rewritten, OR a nested iteration carries
|
|
294
|
-
// scoped handlers (its inlined batchFn would be wrong too). The runtime
|
|
295
|
-
// clone / batch / diff paths all read the rewritten template correctly.
|
|
296
222
|
const hasScopedHandlers =
|
|
297
223
|
(childAliases.size > 0 && templateContainer.innerHTML.includes("$scope(this,")) ||
|
|
298
224
|
subtreeHasScopedHandlers(templateParsed);
|
|
299
225
|
|
|
300
|
-
// All aliases in scope inside this loop (enclosing + this loop's own) —
|
|
301
|
-
// passed back to parse() by iterate.js when it re-parses a cloned
|
|
302
|
-
// instance. Must be the ACCUMULATED set, not just this loop's own: a
|
|
303
|
-
// handler nested in a further if/each inside the loop can reference an
|
|
304
|
-
// outer alias that wasn't rewritten in this loop's own template (the
|
|
305
|
-
// inner structure's branch was extracted to a separate container), so
|
|
306
|
-
// the re-parse needs every enclosing alias to rewrite it.
|
|
307
226
|
const scopeAliases = [...childAliases];
|
|
308
227
|
|
|
309
|
-
// Store iteration metadata (use index for deterministic keys)
|
|
310
228
|
const iterationKey = `iteration_${i}`;
|
|
311
229
|
const iterationNode = {
|
|
312
230
|
type: 'iteration',
|
|
@@ -325,9 +243,6 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
325
243
|
children: templateParsed,
|
|
326
244
|
},
|
|
327
245
|
},
|
|
328
|
-
// Preserve runtime data from previous parse if it exists (stored on startComment by iterate.js)
|
|
329
|
-
// Only restore if the comment node is still connected to the DOM (not replaced by component loading)
|
|
330
|
-
// @ts-ignore - custom property added by iterate.js
|
|
331
246
|
runtime: (element.isConnected && element.__vibeIterationRuntime) || {
|
|
332
247
|
instances: [],
|
|
333
248
|
templateRemoved: false,
|
|
@@ -336,13 +251,11 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
336
251
|
};
|
|
337
252
|
result[iterationKey] = iterationNode;
|
|
338
253
|
|
|
339
|
-
// Mark template indices as processed
|
|
340
254
|
for (let j = i + 1; j < endIndex; j++) {
|
|
341
255
|
skipIndices.add(j);
|
|
342
256
|
}
|
|
343
|
-
skipIndices.add(endIndex);
|
|
257
|
+
skipIndices.add(endIndex);
|
|
344
258
|
|
|
345
|
-
// Skip past this iteration block
|
|
346
259
|
i = endIndex;
|
|
347
260
|
continue;
|
|
348
261
|
} catch (e) {
|
|
@@ -351,37 +264,24 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
351
264
|
}
|
|
352
265
|
}
|
|
353
266
|
|
|
354
|
-
// Handle conditional comments
|
|
355
267
|
const conditionalMatch = textContent.trim().match(CONDITIONAL_REGEX);
|
|
356
268
|
|
|
357
269
|
if (conditionalMatch) {
|
|
358
270
|
const [_, expression] = conditionalMatch;
|
|
359
271
|
|
|
360
272
|
try {
|
|
361
|
-
// Find matching end comment and optional else / else-if boundary
|
|
362
273
|
const { elseIndex, endIndex, elseText } = findConditionalEnd(children, i + 1);
|
|
363
274
|
|
|
364
|
-
// Extract true branch nodes (between if and else/endif)
|
|
365
275
|
const trueBranchEnd = elseIndex !== null ? elseIndex : endIndex;
|
|
366
276
|
const trueBranchNodes = Array.from(children).slice(i + 1, trueBranchEnd);
|
|
367
277
|
|
|
368
|
-
// Create temporary container for true branch
|
|
369
278
|
const trueBranchContainer = document.createElement('div');
|
|
370
279
|
trueBranchNodes.forEach((node) => {
|
|
371
280
|
trueBranchContainer.appendChild(node.cloneNode(true));
|
|
372
281
|
});
|
|
373
282
|
|
|
374
|
-
// Parse the true branch recursively (loop aliases stay in scope
|
|
375
|
-
// inside a conditional nested within an iteration).
|
|
376
283
|
const trueBranchParsed = recursive([...trueBranchContainer.childNodes], undefined, new Set(), stats, aliasSet);
|
|
377
284
|
|
|
378
|
-
// Extract false branch nodes if else exists. An `else if` boundary
|
|
379
|
-
// DESUGARS: the false branch becomes a synthesized nested
|
|
380
|
-
// conditional (`<!-- if rest -->…<!-- /if -->` wrapping the chain
|
|
381
|
-
// tail), so every mount/patch/manifest path only ever sees binary
|
|
382
|
-
// conditionals — else-if is grammar, not machinery. The chain tail
|
|
383
|
-
// may itself contain further else-if boundaries; the recursion
|
|
384
|
-
// desugars them the same way.
|
|
385
285
|
let falseBranchParsed = null;
|
|
386
286
|
let falseBranchContainer = null;
|
|
387
287
|
if (elseIndex !== null) {
|
|
@@ -400,16 +300,12 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
400
300
|
falseBranchParsed = recursive([...falseBranchContainer.childNodes], undefined, new Set(), stats, aliasSet);
|
|
401
301
|
}
|
|
402
302
|
|
|
403
|
-
// Store conditional metadata (use index for deterministic keys)
|
|
404
303
|
const conditionalKey = `conditional_${i}`;
|
|
405
304
|
result[conditionalKey] = {
|
|
406
305
|
_key: conditionalKey,
|
|
407
306
|
type: 'conditional',
|
|
408
307
|
meta: {
|
|
409
308
|
expression,
|
|
410
|
-
// Enclosing loop aliases — threaded back into parse() when
|
|
411
|
-
// mountBranch re-parses this branch, so loop-scoped handlers
|
|
412
|
-
// (including those nested deeper in further conditionals) rewrite.
|
|
413
309
|
scopeAliases: [...aliasSet],
|
|
414
310
|
startComment: element,
|
|
415
311
|
elseComment: elseIndex !== null ? children[elseIndex] : null,
|
|
@@ -437,12 +333,10 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
437
333
|
children: {},
|
|
438
334
|
};
|
|
439
335
|
|
|
440
|
-
// Mark template indices as processed
|
|
441
336
|
for (let j = i + 1; j <= endIndex; j++) {
|
|
442
337
|
skipIndices.add(j);
|
|
443
338
|
}
|
|
444
339
|
|
|
445
|
-
// Skip past this conditional block
|
|
446
340
|
i = endIndex;
|
|
447
341
|
continue;
|
|
448
342
|
} catch (e) {
|
|
@@ -451,17 +345,15 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
451
345
|
}
|
|
452
346
|
}
|
|
453
347
|
|
|
454
|
-
// Skip other comments
|
|
455
348
|
continue;
|
|
456
349
|
}
|
|
457
350
|
|
|
458
351
|
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
|
|
459
352
|
const nodeIdentifier = `${name}_${i}`.toLowerCase();
|
|
460
353
|
|
|
461
|
-
// For text nodes, use parent element (text nodes can't have attributes)
|
|
462
354
|
const isTextNode = nodeName === '#text';
|
|
463
355
|
const elementForBindings = isTextNode ? element.parentElement : element;
|
|
464
|
-
const textNodeRef = isTextNode ? element : null;
|
|
356
|
+
const textNodeRef = isTextNode ? element : null;
|
|
465
357
|
|
|
466
358
|
const { attributes, nameBindings } = captureAttributeBindings(element, aliasSet);
|
|
467
359
|
const hasChildren = childNodes.length;
|
|
@@ -493,11 +385,6 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
493
385
|
return result;
|
|
494
386
|
};
|
|
495
387
|
|
|
496
|
-
// `aliasSet` carries enclosing `<!-- each -->` aliases into the parse. The root
|
|
497
|
-
// page parse passes none; iterate.js passes a loop's own aliases when it
|
|
498
|
-
// re-parses a cloned instance subtree, so loop-scoped `on*` handlers (including
|
|
499
|
-
// those in nested loops, via child-alias accumulation in recursive) rewrite to
|
|
500
|
-
// `$scope(this,'alias')`.
|
|
501
388
|
export default (root, rootKey = undefined, aliasSet = new Set()) => {
|
|
502
389
|
const { childNodes } = root;
|
|
503
390
|
const stats = { skipped: 0 };
|
|
@@ -505,7 +392,6 @@ export default (root, rootKey = undefined, aliasSet = new Set()) => {
|
|
|
505
392
|
const { attributes, nameBindings } = captureAttributeBindings(root, aliasSet);
|
|
506
393
|
|
|
507
394
|
return {
|
|
508
|
-
// html: root.outerHTML,
|
|
509
395
|
parsed: parseHTML([...childNodes], rootKey),
|
|
510
396
|
element: root,
|
|
511
397
|
children: recursive(Array.from(childNodes), rootKey, new Set(), stats, aliasSet),
|
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pre-compiled-iterations.js
|
|
3
|
-
*
|
|
4
|
-
* Pre-compiled iteration rendering - production version of iteration optimization.
|
|
5
|
-
* Uses pre-compiled batch functions from manifest generated by the compiler.
|
|
6
|
-
*
|
|
7
|
-
* This is the production implementation that runs compiled code generated at build time.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { stampInstanceScopes } from './loop-scope.js';
|
|
2
|
+
import { markBoundValues } from './iteration-utils.js';
|
|
11
3
|
|
|
12
|
-
// Reusable template element for parsing compiled HTML
|
|
13
4
|
const parseTemplate = typeof document !== 'undefined' ? document.createElement('template') : null;
|
|
14
5
|
|
|
15
|
-
/**
|
|
16
|
-
* Check if template has nested iterations or conditionals
|
|
17
|
-
*/
|
|
18
6
|
const hasNestedStructures = (template) => {
|
|
19
7
|
if (!template || !template.children) return false;
|
|
20
8
|
for (const key in template.children) {
|
|
@@ -26,37 +14,23 @@ const hasNestedStructures = (template) => {
|
|
|
26
14
|
return false;
|
|
27
15
|
};
|
|
28
16
|
|
|
29
|
-
/**
|
|
30
|
-
* Check if iteration node has compiled batch function
|
|
31
|
-
*/
|
|
32
17
|
export const canUseCompiled = (iterationNode) => {
|
|
33
18
|
if (!iterationNode || !iterationNode.meta) {
|
|
34
19
|
return false;
|
|
35
20
|
}
|
|
36
21
|
|
|
37
|
-
// A template with a loop-scoped `$scope(this,'alias')` handler was rewritten at
|
|
38
|
-
// parse time; the manifest's compiled batchFn predates that rewrite and would
|
|
39
|
-
// emit the bare, unresolvable alias. Fall back to the runtime path, which reads
|
|
40
|
-
// the rewritten template.
|
|
41
22
|
if (iterationNode.meta.hasScopedHandlers) {
|
|
42
23
|
return false;
|
|
43
24
|
}
|
|
44
25
|
|
|
45
|
-
// Check compiled data from manifest merge
|
|
46
26
|
const compiled = iterationNode.compiled;
|
|
47
27
|
if (!compiled || !compiled.iterations || !compiled.iterations.batchFn) {
|
|
48
28
|
return false;
|
|
49
29
|
}
|
|
50
30
|
|
|
51
|
-
// If we have a compiled batch function, we can use it even if the template
|
|
52
|
-
// has nested structures, because the compiler has already inlined them
|
|
53
|
-
// into the batch function
|
|
54
31
|
return true;
|
|
55
32
|
};
|
|
56
33
|
|
|
57
|
-
/**
|
|
58
|
-
* Get compiled function metadata from iteration node
|
|
59
|
-
*/
|
|
60
34
|
export const getCompiledMeta = (iterationNode) => {
|
|
61
35
|
const compiled = iterationNode.compiled;
|
|
62
36
|
if (!compiled || !compiled.iterations) return null;
|
|
@@ -68,12 +42,6 @@ export const getCompiledMeta = (iterationNode) => {
|
|
|
68
42
|
};
|
|
69
43
|
};
|
|
70
44
|
|
|
71
|
-
/**
|
|
72
|
-
* Build a scoped wrapper that puts state keys in scope for the batch function.
|
|
73
|
-
* The compiler generates (arr, $) => { ... } with bare variable names like `selectedCategory`,
|
|
74
|
-
* but those aren't parameters of the arrow function. We create a wrapper that defines
|
|
75
|
-
* state keys as parameters, then evaluates the batch function in that scope.
|
|
76
|
-
*/
|
|
77
45
|
const buildScopedFn = (batchFnStr, stateKeys) => {
|
|
78
46
|
return new Function(
|
|
79
47
|
...stateKeys, 'arr',
|
|
@@ -83,9 +51,6 @@ const buildScopedFn = (batchFnStr, stateKeys) => {
|
|
|
83
51
|
);
|
|
84
52
|
};
|
|
85
53
|
|
|
86
|
-
/**
|
|
87
|
-
* Call a compiled batch function with state keys spread into scope
|
|
88
|
-
*/
|
|
89
54
|
const callCompiled = (iterationNode, array, state, compiledMeta) => {
|
|
90
55
|
if (!iterationNode.runtime.compiledFn) {
|
|
91
56
|
try {
|
|
@@ -103,20 +68,16 @@ const callCompiled = (iterationNode, array, state, compiledMeta) => {
|
|
|
103
68
|
return iterationNode.runtime.compiledFn(...values, array);
|
|
104
69
|
};
|
|
105
70
|
|
|
106
|
-
/**
|
|
107
|
-
* Render iteration using pre-compiled batch function from manifest
|
|
108
|
-
*/
|
|
109
71
|
export const renderCompiled = (iterationNode, array, state, compiledMeta, parent, endComment) => {
|
|
110
72
|
const html = callCompiled(iterationNode, array, state, compiledMeta);
|
|
111
73
|
if (html === null) return false;
|
|
112
74
|
|
|
113
|
-
// Parse and insert
|
|
114
75
|
if (parseTemplate) {
|
|
115
76
|
parseTemplate.innerHTML = html;
|
|
116
77
|
const frag = parseTemplate.content;
|
|
78
|
+
markBoundValues(frag, html);
|
|
117
79
|
const kids = frag.children;
|
|
118
80
|
|
|
119
|
-
// Track instances - pre-allocate array for performance
|
|
120
81
|
const arrayLen = array.length;
|
|
121
82
|
const instances = new Array(arrayLen);
|
|
122
83
|
for (let i = 0; i < arrayLen; i++) {
|
|
@@ -133,9 +94,6 @@ export const renderCompiled = (iterationNode, array, state, compiledMeta, parent
|
|
|
133
94
|
return false;
|
|
134
95
|
};
|
|
135
96
|
|
|
136
|
-
/**
|
|
137
|
-
* Update iteration using pre-compiled batch function (bulk rebuild)
|
|
138
|
-
*/
|
|
139
97
|
export const updateCompiled = (iterationNode, newArray, state, compiledMeta, startComment, endComment) => {
|
|
140
98
|
const parent = startComment.parentNode;
|
|
141
99
|
const rt = iterationNode.runtime;
|
|
@@ -143,16 +101,11 @@ export const updateCompiled = (iterationNode, newArray, state, compiledMeta, sta
|
|
|
143
101
|
const html = newArray.length === 0 ? '' : callCompiled(iterationNode, newArray, state, compiledMeta);
|
|
144
102
|
if (html === null) return false;
|
|
145
103
|
|
|
146
|
-
// Keep-DOM guard, same contract as the runtime batch renderer: compiled
|
|
147
|
-
// iterations sit in the always-dirty bucket, so every flush of ANY key
|
|
148
|
-
// lands here — identical output must keep the existing rows (imperative
|
|
149
|
-
// listeners, focus, markers die with deleteContents), not rebuild them.
|
|
150
104
|
if (html === rt.lastCompiledHtml && rt.instances.length === newArray.length) {
|
|
151
105
|
for (let i = 0; i < newArray.length; i++) rt.instances[i].item = newArray[i];
|
|
152
106
|
return true;
|
|
153
107
|
}
|
|
154
108
|
|
|
155
|
-
// Clear existing instances
|
|
156
109
|
if (rt.instances.length > 0) {
|
|
157
110
|
const range = document.createRange();
|
|
158
111
|
range.setStartAfter(startComment);
|
|
@@ -166,13 +119,12 @@ export const updateCompiled = (iterationNode, newArray, state, compiledMeta, sta
|
|
|
166
119
|
return true;
|
|
167
120
|
}
|
|
168
121
|
|
|
169
|
-
// Parse and insert
|
|
170
122
|
if (parseTemplate) {
|
|
171
123
|
parseTemplate.innerHTML = html;
|
|
172
124
|
const frag = parseTemplate.content;
|
|
125
|
+
markBoundValues(frag, html);
|
|
173
126
|
const kids = frag.children;
|
|
174
127
|
|
|
175
|
-
// Track instances - pre-allocate array for performance
|
|
176
128
|
const arrayLen = newArray.length;
|
|
177
129
|
const instances = new Array(arrayLen);
|
|
178
130
|
for (let i = 0; i < arrayLen; i++) {
|