@mintjamsinc/ichigojs 0.1.57 → 0.1.59
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 +31 -0
- package/dist/ichigo.cjs +162 -70
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +162 -70
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.min.cjs +1 -1
- package/dist/ichigo.umd.js +162 -70
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VNode.d.ts +8 -0
- package/dist/types/ichigo/util/VFragmentRange.d.ts +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -219,6 +219,37 @@ List rendering:
|
|
|
219
219
|
</ul>
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
##### Using `<template>` as a fragment
|
|
223
|
+
|
|
224
|
+
`v-for` and `v-if` can be placed on a `<template>` element to render
|
|
225
|
+
multiple nodes per iteration without introducing a wrapper element:
|
|
226
|
+
|
|
227
|
+
```html
|
|
228
|
+
<dl>
|
|
229
|
+
<template v-for="item in items" :key="item.id">
|
|
230
|
+
<dt>{{ item.term }}</dt>
|
|
231
|
+
<dd>{{ item.description }}</dd>
|
|
232
|
+
</template>
|
|
233
|
+
</dl>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`<template>` supports **either** `v-for` **or** `v-if` per element, but
|
|
237
|
+
**not both on the same `<template>`**. If you need both, either:
|
|
238
|
+
|
|
239
|
+
1. Nest them on separate `<template>` / element levels:
|
|
240
|
+
```html
|
|
241
|
+
<template v-for="item in items" :key="item.id">
|
|
242
|
+
<div v-if="item.visible">{{ item.name }}</div>
|
|
243
|
+
</template>
|
|
244
|
+
```
|
|
245
|
+
2. Use a regular element instead of `<template>`:
|
|
246
|
+
```html
|
|
247
|
+
<div v-for="item in items" v-if="item.visible" :key="item.id">...</div>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Combining `v-for` and `v-if` on the same element works for all
|
|
251
|
+
non-`<template>` tags (v-for is evaluated first, v-if per iteration).
|
|
252
|
+
|
|
222
253
|
#### v-show
|
|
223
254
|
|
|
224
255
|
Toggle visibility:
|
package/dist/ichigo.cjs
CHANGED
|
@@ -8653,6 +8653,14 @@
|
|
|
8653
8653
|
// Other directives (@click, :class, etc.) will be processed on the cloned/rendered elements.
|
|
8654
8654
|
const attributes = [];
|
|
8655
8655
|
if (element.hasAttribute(StandardDirectiveName.V_FOR)) {
|
|
8656
|
+
// <template v-for v-if> is not supported: v-for clones .content (a
|
|
8657
|
+
// DocumentFragment), so the v-if attribute on the <template> itself
|
|
8658
|
+
// is lost. Warn the developer so silent failure is avoided.
|
|
8659
|
+
if (element instanceof HTMLTemplateElement && element.hasAttribute(StandardDirectiveName.V_IF)) {
|
|
8660
|
+
console.warn('[ichigo.js] <template> cannot combine v-for and v-if. ' +
|
|
8661
|
+
'The v-if will be ignored. Move v-if to an inner element, ' +
|
|
8662
|
+
'or replace <template> with a regular element.', element);
|
|
8663
|
+
}
|
|
8656
8664
|
// For v-for template element: only process v-for and :key
|
|
8657
8665
|
// Other attributes will be processed when child VNodes are created for cloned elements
|
|
8658
8666
|
attributes.push(element.getAttributeNode(StandardDirectiveName.V_FOR));
|
|
@@ -8961,6 +8969,13 @@
|
|
|
8961
8969
|
* VNode is destroyed.
|
|
8962
8970
|
*/
|
|
8963
8971
|
#userData;
|
|
8972
|
+
/**
|
|
8973
|
+
* When this VNode wraps a <template>-derived DocumentFragment that has been
|
|
8974
|
+
* expanded into the DOM, this range tracks the start/end boundary of the
|
|
8975
|
+
* expanded content. Directives such as v-for and v-if use this to move or
|
|
8976
|
+
* remove the entire fragment atomically.
|
|
8977
|
+
*/
|
|
8978
|
+
#fragmentRange;
|
|
8964
8979
|
/**
|
|
8965
8980
|
* Creates an instance of the virtual node.
|
|
8966
8981
|
* @param args The initialization arguments for the virtual node.
|
|
@@ -9196,6 +9211,17 @@
|
|
|
9196
9211
|
}
|
|
9197
9212
|
return this.#userData;
|
|
9198
9213
|
}
|
|
9214
|
+
/**
|
|
9215
|
+
* The DOM range that bounds this VNode's expanded fragment content, if any.
|
|
9216
|
+
* Set by directives that expand a <template> into a DocumentFragment
|
|
9217
|
+
* (currently v-for and v-if).
|
|
9218
|
+
*/
|
|
9219
|
+
get fragmentRange() {
|
|
9220
|
+
return this.#fragmentRange;
|
|
9221
|
+
}
|
|
9222
|
+
set fragmentRange(range) {
|
|
9223
|
+
this.#fragmentRange = range;
|
|
9224
|
+
}
|
|
9199
9225
|
/**
|
|
9200
9226
|
* The DOM path of this virtual node.
|
|
9201
9227
|
* This is a string representation of the path from the root to this node,
|
|
@@ -9541,6 +9567,102 @@
|
|
|
9541
9567
|
}
|
|
9542
9568
|
}
|
|
9543
9569
|
|
|
9570
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
9571
|
+
/**
|
|
9572
|
+
* Represents a contiguous range of DOM nodes that conceptually belong to a single VNode
|
|
9573
|
+
* whose source is a <template> element (and therefore expands to multiple sibling nodes
|
|
9574
|
+
* when inserted into the DOM).
|
|
9575
|
+
*
|
|
9576
|
+
* The range is bounded by two comment markers (start and end). All nodes between the
|
|
9577
|
+
* markers (inclusive) form the range. Move and remove operations always act on the
|
|
9578
|
+
* entire range atomically via the DOM Range API, which prevents the marker pair from
|
|
9579
|
+
* becoming desynchronized from the content it bounds.
|
|
9580
|
+
*
|
|
9581
|
+
* This abstraction replaces ad-hoc per-marker bookkeeping previously stored in
|
|
9582
|
+
* {@link VNode.userData} by directives such as v-for and v-if.
|
|
9583
|
+
*/
|
|
9584
|
+
class VFragmentRange {
|
|
9585
|
+
#start;
|
|
9586
|
+
#end;
|
|
9587
|
+
constructor(start, end) {
|
|
9588
|
+
this.#start = start;
|
|
9589
|
+
this.#end = end;
|
|
9590
|
+
}
|
|
9591
|
+
/**
|
|
9592
|
+
* Inserts content into a parent before the given reference sibling (or as last
|
|
9593
|
+
* child when {@code refSibling} is null), wrapping it with start/end comment
|
|
9594
|
+
* markers. Returns a {@link VFragmentRange} that tracks the inserted region.
|
|
9595
|
+
*
|
|
9596
|
+
* @param parent The parent node to insert into.
|
|
9597
|
+
* @param refSibling The sibling to insert before (null = append).
|
|
9598
|
+
* @param label A short label used for the marker comment text (helps debugging).
|
|
9599
|
+
* @param content The content to insert. Typically a DocumentFragment cloned
|
|
9600
|
+
* from a template's content, but any Node works.
|
|
9601
|
+
*/
|
|
9602
|
+
static insert(parent, refSibling, label, content) {
|
|
9603
|
+
const start = document.createComment(`#${label}-start`);
|
|
9604
|
+
const end = document.createComment(`#${label}-end`);
|
|
9605
|
+
parent.insertBefore(start, refSibling);
|
|
9606
|
+
parent.insertBefore(end, refSibling);
|
|
9607
|
+
parent.insertBefore(content, end);
|
|
9608
|
+
return new VFragmentRange(start, end);
|
|
9609
|
+
}
|
|
9610
|
+
/**
|
|
9611
|
+
* The start marker (inclusive boundary).
|
|
9612
|
+
*/
|
|
9613
|
+
get firstNode() {
|
|
9614
|
+
return this.#start;
|
|
9615
|
+
}
|
|
9616
|
+
/**
|
|
9617
|
+
* The end marker (inclusive boundary).
|
|
9618
|
+
*/
|
|
9619
|
+
get lastNode() {
|
|
9620
|
+
return this.#end;
|
|
9621
|
+
}
|
|
9622
|
+
/**
|
|
9623
|
+
* Move the entire range (start marker through end marker, inclusive) to be
|
|
9624
|
+
* inserted before {@code refSibling} under {@code parent}. If {@code refSibling}
|
|
9625
|
+
* is null the range is appended.
|
|
9626
|
+
*
|
|
9627
|
+
* Implemented via the DOM Range API so that all nodes between the markers move
|
|
9628
|
+
* together as a single contiguous block, regardless of how many or which kinds
|
|
9629
|
+
* of nodes currently sit between them.
|
|
9630
|
+
*/
|
|
9631
|
+
moveBefore(parent, refSibling) {
|
|
9632
|
+
// No-op if already in the desired position
|
|
9633
|
+
if (refSibling === this.#start) {
|
|
9634
|
+
return;
|
|
9635
|
+
}
|
|
9636
|
+
const range = this.#asDomRange();
|
|
9637
|
+
const fragment = range.extractContents();
|
|
9638
|
+
parent.insertBefore(fragment, refSibling);
|
|
9639
|
+
range.detach();
|
|
9640
|
+
}
|
|
9641
|
+
/**
|
|
9642
|
+
* Remove the entire range (start marker through end marker, inclusive) from its
|
|
9643
|
+
* current parent. Safe to call even if the markers are already detached.
|
|
9644
|
+
*/
|
|
9645
|
+
remove() {
|
|
9646
|
+
if (!this.#start.parentNode) {
|
|
9647
|
+
return;
|
|
9648
|
+
}
|
|
9649
|
+
const range = this.#asDomRange();
|
|
9650
|
+
range.deleteContents();
|
|
9651
|
+
range.detach();
|
|
9652
|
+
}
|
|
9653
|
+
/**
|
|
9654
|
+
* Builds a DOM {@link Range} that spans from immediately before the start marker
|
|
9655
|
+
* to immediately after the end marker, covering both markers and everything
|
|
9656
|
+
* between them.
|
|
9657
|
+
*/
|
|
9658
|
+
#asDomRange() {
|
|
9659
|
+
const range = document.createRange();
|
|
9660
|
+
range.setStartBefore(this.#start);
|
|
9661
|
+
range.setEndAfter(this.#end);
|
|
9662
|
+
return range;
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
|
|
9544
9666
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
9545
9667
|
/**
|
|
9546
9668
|
* Base class for conditional directives such as v-if, v-else-if, and v-else.
|
|
@@ -9741,19 +9863,7 @@
|
|
|
9741
9863
|
const anchorParent = this.#vNode.anchorNode?.parentNode;
|
|
9742
9864
|
const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
|
|
9743
9865
|
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
|
|
9744
|
-
|
|
9745
|
-
const endMarker = document.createComment('#vif-fragment-end');
|
|
9746
|
-
if (nextSibling) {
|
|
9747
|
-
anchorParent.insertBefore(startMarker, nextSibling);
|
|
9748
|
-
}
|
|
9749
|
-
else {
|
|
9750
|
-
anchorParent.appendChild(startMarker);
|
|
9751
|
-
}
|
|
9752
|
-
anchorParent.insertBefore(endMarker, startMarker.nextSibling);
|
|
9753
|
-
anchorParent.insertBefore(clone, endMarker);
|
|
9754
|
-
// Store markers for later removal
|
|
9755
|
-
vNode.userData.set('vif_fragment_start', startMarker);
|
|
9756
|
-
vNode.userData.set('vif_fragment_end', endMarker);
|
|
9866
|
+
vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
|
|
9757
9867
|
this.#renderedVNode = vNode;
|
|
9758
9868
|
this.#renderedVNode.forceUpdate();
|
|
9759
9869
|
return;
|
|
@@ -9774,19 +9884,11 @@
|
|
|
9774
9884
|
}
|
|
9775
9885
|
// Destroy VNode first (calls @unmount hooks while DOM is still accessible)
|
|
9776
9886
|
this.#renderedVNode.destroy();
|
|
9777
|
-
// Then remove from DOM. Handle fragment
|
|
9778
|
-
const
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
let node = startMarker;
|
|
9783
|
-
while (node) {
|
|
9784
|
-
const next = node.nextSibling;
|
|
9785
|
-
parentNode.removeChild(node);
|
|
9786
|
-
if (node === endMarker)
|
|
9787
|
-
break;
|
|
9788
|
-
node = next;
|
|
9789
|
-
}
|
|
9887
|
+
// Then remove from DOM. Handle fragment ranges if present.
|
|
9888
|
+
const range = this.#renderedVNode.fragmentRange;
|
|
9889
|
+
if (range) {
|
|
9890
|
+
range.remove();
|
|
9891
|
+
this.#renderedVNode.fragmentRange = undefined;
|
|
9790
9892
|
this.#renderedVNode = undefined;
|
|
9791
9893
|
return;
|
|
9792
9894
|
}
|
|
@@ -10052,6 +10154,12 @@
|
|
|
10052
10154
|
}
|
|
10053
10155
|
// Then remove DOM nodes
|
|
10054
10156
|
for (const vNode of this.#renderedItems.values()) {
|
|
10157
|
+
const range = vNode.fragmentRange;
|
|
10158
|
+
if (range) {
|
|
10159
|
+
range.remove();
|
|
10160
|
+
vNode.fragmentRange = undefined;
|
|
10161
|
+
continue;
|
|
10162
|
+
}
|
|
10055
10163
|
if (vNode.node.parentNode) {
|
|
10056
10164
|
vNode.node.parentNode.removeChild(vNode.node);
|
|
10057
10165
|
}
|
|
@@ -10125,22 +10233,12 @@
|
|
|
10125
10233
|
vNode.destroy();
|
|
10126
10234
|
}
|
|
10127
10235
|
}
|
|
10128
|
-
// Then remove from DOM. Handle both Element nodes and fragment
|
|
10236
|
+
// Then remove from DOM. Handle both Element nodes and fragment ranges.
|
|
10129
10237
|
for (const vNode of nodesToRemove) {
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
const parentNode = startMarker.parentNode;
|
|
10135
|
-
let node = startMarker;
|
|
10136
|
-
// Remove nodes from startMarker up to and including endMarker
|
|
10137
|
-
while (node) {
|
|
10138
|
-
const next = node.nextSibling;
|
|
10139
|
-
parentNode.removeChild(node);
|
|
10140
|
-
if (node === endMarker)
|
|
10141
|
-
break;
|
|
10142
|
-
node = next;
|
|
10143
|
-
}
|
|
10238
|
+
const range = vNode.fragmentRange;
|
|
10239
|
+
if (range) {
|
|
10240
|
+
range.remove();
|
|
10241
|
+
vNode.fragmentRange = undefined;
|
|
10144
10242
|
continue;
|
|
10145
10243
|
}
|
|
10146
10244
|
// Fallback: remove the node itself if it's attached
|
|
@@ -10180,26 +10278,14 @@
|
|
|
10180
10278
|
bindings,
|
|
10181
10279
|
dependentIdentifiers: depIds,
|
|
10182
10280
|
});
|
|
10183
|
-
// If clone is a DocumentFragment,
|
|
10281
|
+
// If clone is a DocumentFragment, wrap it in a VFragmentRange so the
|
|
10282
|
+
// entire expanded content moves/removes as one atomic unit.
|
|
10184
10283
|
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
10185
|
-
const
|
|
10186
|
-
|
|
10187
|
-
// Insert start and end markers and the fragment's children between them
|
|
10188
|
-
if (prevNode.nextSibling) {
|
|
10189
|
-
parent.insertBefore(startMarker, prevNode.nextSibling);
|
|
10190
|
-
}
|
|
10191
|
-
else {
|
|
10192
|
-
parent.appendChild(startMarker);
|
|
10193
|
-
}
|
|
10194
|
-
parent.insertBefore(endMarker, startMarker.nextSibling);
|
|
10195
|
-
parent.insertBefore(clone, endMarker);
|
|
10196
|
-
// Store markers on the VNode for later removal/movement
|
|
10197
|
-
vNode.userData.set('vfor_fragment_start', startMarker);
|
|
10198
|
-
vNode.userData.set('vfor_fragment_end', endMarker);
|
|
10284
|
+
const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
|
|
10285
|
+
vNode.fragmentRange = range;
|
|
10199
10286
|
newRenderedItems.set(key, vNode);
|
|
10200
10287
|
vNode.forceUpdate();
|
|
10201
|
-
|
|
10202
|
-
prevNode = endMarker;
|
|
10288
|
+
prevNode = range.lastNode;
|
|
10203
10289
|
continue;
|
|
10204
10290
|
}
|
|
10205
10291
|
// Determine what to insert: anchor node (if exists) or the clone itself
|
|
@@ -10219,22 +10305,28 @@
|
|
|
10219
10305
|
newRenderedItems.set(key, vNode);
|
|
10220
10306
|
// Update bindings
|
|
10221
10307
|
this.#updateItemBindings(vNode, context);
|
|
10222
|
-
//
|
|
10223
|
-
|
|
10224
|
-
const
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
parent.insertBefore(actualNode, prevNode.nextSibling);
|
|
10308
|
+
// For fragment-backed iterations, move the entire range atomically.
|
|
10309
|
+
// For single-node iterations, move just the node.
|
|
10310
|
+
const range = vNode.fragmentRange;
|
|
10311
|
+
if (range) {
|
|
10312
|
+
if (prevNode.nextSibling !== range.firstNode) {
|
|
10313
|
+
range.moveBefore(parent, prevNode.nextSibling);
|
|
10229
10314
|
}
|
|
10230
|
-
|
|
10231
|
-
|
|
10315
|
+
}
|
|
10316
|
+
else {
|
|
10317
|
+
const actualNode = vNode.anchorNode || vNode.node;
|
|
10318
|
+
if (prevNode.nextSibling !== actualNode) {
|
|
10319
|
+
if (prevNode.nextSibling) {
|
|
10320
|
+
parent.insertBefore(actualNode, prevNode.nextSibling);
|
|
10321
|
+
}
|
|
10322
|
+
else {
|
|
10323
|
+
parent.appendChild(actualNode);
|
|
10324
|
+
}
|
|
10232
10325
|
}
|
|
10233
10326
|
}
|
|
10234
10327
|
}
|
|
10235
|
-
//
|
|
10236
|
-
|
|
10237
|
-
prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
|
|
10328
|
+
// Advance prevNode to this iteration's last DOM node
|
|
10329
|
+
prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
|
|
10238
10330
|
}
|
|
10239
10331
|
// Update rendered items map
|
|
10240
10332
|
this.#renderedItems = newRenderedItems;
|