@mintjamsinc/ichigojs 0.1.68 → 0.1.69
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 +293 -5
- package/dist/ichigo.cjs +67 -24
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +67 -24
- 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 +67 -24
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,14 +9,17 @@ A simple and intuitive reactive framework. Lightweight, fast, and user-friendly
|
|
|
9
9
|
|
|
10
10
|
- ✨ **Vue-like API** - Familiar syntax for Vue developers
|
|
11
11
|
- ⚡ **Reactive Proxy System** - Automatic change detection without manual triggers
|
|
12
|
-
- 🎯 **Computed Properties** - Automatic dependency tracking and re-evaluation
|
|
12
|
+
- 🎯 **Computed Properties** - Automatic dependency tracking and re-evaluation, including writable computed (`{ get, set }`)
|
|
13
|
+
- 👀 **Watchers** - React to data changes with the `watch` option (`deep`, `immediate`)
|
|
13
14
|
- 🔄 **Two-way Binding** - `v-model` with modifiers (`.lazy`, `.number`, `.trim`)
|
|
14
15
|
- 🔌 **Lifecycle Hooks** - `@mount`, `@mounted`, `@update`, `@updated`, `@unmount`, `@unmounted` with context (`$ctx`)
|
|
15
16
|
- 💾 **userData Storage** - Proxy-free storage for third-party library instances with auto-cleanup
|
|
17
|
+
- 🧩 **Components** - Reusable Web Components via `defineComponent` with `props`, `slot`, and `$emit`
|
|
16
18
|
- 📦 **Lightweight** - Minimal bundle size
|
|
17
19
|
- 🚀 **High Performance** - Efficient batched updates via microtask queue
|
|
18
20
|
- 💪 **TypeScript** - Written in TypeScript with full type support
|
|
19
|
-
- 🎨 **Directives** - `v-if`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-resize`, `v-intersection`, `v-performance`
|
|
21
|
+
- 🎨 **Directives** - `v-if`, `v-else-if`, `v-else`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-text`, `v-html`, `v-focus`, `v-resize`, `v-intersection`, `v-performance`
|
|
22
|
+
- 🎯 **Focus Management** - Declarative focus control with the `v-focus` directive (`.select`, `.cursor-end`)
|
|
20
23
|
- 📐 **Resize Observer** - Monitor element size changes with `v-resize` directive
|
|
21
24
|
- 👁️ **Intersection Observer** - Detect element visibility with `v-intersection` directive
|
|
22
25
|
- ⚡ **Performance Observer** - Monitor performance metrics with `v-performance` directive
|
|
@@ -195,6 +198,84 @@ VDOM.createApp({
|
|
|
195
198
|
}).mount('#app');
|
|
196
199
|
```
|
|
197
200
|
|
|
201
|
+
**Writable Computed Properties:**
|
|
202
|
+
|
|
203
|
+
A computed property can also be defined as an object with both a `get` and a
|
|
204
|
+
`set` function. This makes it writable, so it can be used as a `v-model` target
|
|
205
|
+
or assigned to directly. Reads go through `get`, while assignments are routed
|
|
206
|
+
through `set`.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
VDOM.createApp({
|
|
210
|
+
data() {
|
|
211
|
+
return {
|
|
212
|
+
firstName: 'John',
|
|
213
|
+
lastName: 'Doe'
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
computed: {
|
|
217
|
+
fullName: {
|
|
218
|
+
get() {
|
|
219
|
+
return `${this.firstName} ${this.lastName}`;
|
|
220
|
+
},
|
|
221
|
+
set(value) {
|
|
222
|
+
const [first, last] = value.split(' ');
|
|
223
|
+
this.firstName = first;
|
|
224
|
+
this.lastName = last;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}).mount('#app');
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<!-- Assigning through v-model invokes the computed setter -->
|
|
233
|
+
<input v-model="fullName">
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Watchers
|
|
237
|
+
|
|
238
|
+
Use the `watch` option to run a callback whenever a watched property changes.
|
|
239
|
+
Keys are property paths (e.g. `"count"`, `"user.name"`), and the callback
|
|
240
|
+
receives the new and previous values.
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
VDOM.createApp({
|
|
244
|
+
data() {
|
|
245
|
+
return {
|
|
246
|
+
count: 0,
|
|
247
|
+
user: { name: 'Alice' }
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
watch: {
|
|
251
|
+
// Shorthand: a callback function
|
|
252
|
+
count(newValue, oldValue) {
|
|
253
|
+
console.log(`count changed from ${oldValue} to ${newValue}`);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// Watch a nested property by path
|
|
257
|
+
'user.name'(newValue, oldValue) {
|
|
258
|
+
console.log(`name changed from ${oldValue} to ${newValue}`);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Full form: an options object with deep / immediate
|
|
262
|
+
user: {
|
|
263
|
+
handler(newValue, oldValue) {
|
|
264
|
+
console.log('user object changed', newValue);
|
|
265
|
+
},
|
|
266
|
+
deep: true, // Observe nested changes inside the object
|
|
267
|
+
immediate: true // Invoke once immediately with the current value
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}).mount('#app');
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Watcher options:**
|
|
274
|
+
|
|
275
|
+
- `handler` - The callback invoked when the watched value changes
|
|
276
|
+
- `deep` - When `true`, deeply observes nested object/array changes (default: `false`)
|
|
277
|
+
- `immediate` - When `true`, invokes the handler once immediately on registration with the current value (default: `false`)
|
|
278
|
+
|
|
198
279
|
### Directives
|
|
199
280
|
|
|
200
281
|
#### v-if / v-else-if / v-else
|
|
@@ -314,6 +395,73 @@ methods: {
|
|
|
314
395
|
}
|
|
315
396
|
```
|
|
316
397
|
|
|
398
|
+
#### v-text
|
|
399
|
+
|
|
400
|
+
Set the text content of an element. The expression result replaces the
|
|
401
|
+
element's `textContent`. Unlike `v-html`, the content is rendered as plain
|
|
402
|
+
text, so HTML is escaped and XSS is not a concern.
|
|
403
|
+
|
|
404
|
+
```html
|
|
405
|
+
<span v-text="message"></span>
|
|
406
|
+
|
|
407
|
+
<!-- Equivalent to -->
|
|
408
|
+
<span>{{ message }}</span>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Use `v-text` when you want to set the entire text content of an element from a
|
|
412
|
+
single expression (it overwrites any existing content), rather than
|
|
413
|
+
interpolating with `{{ }}`.
|
|
414
|
+
|
|
415
|
+
#### v-html
|
|
416
|
+
|
|
417
|
+
Set the raw HTML content of an element. The expression result is assigned to
|
|
418
|
+
the element's `innerHTML`.
|
|
419
|
+
|
|
420
|
+
```html
|
|
421
|
+
<div v-html="htmlContent"></div>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
> ⚠️ **Security warning:** Dynamically rendering arbitrary HTML can easily lead
|
|
425
|
+
> to XSS attacks. Only use `v-html` on **trusted** content, and **never** on
|
|
426
|
+
> user-provided content. For plain text, use `v-text` or `{{ }}` interpolation
|
|
427
|
+
> instead.
|
|
428
|
+
|
|
429
|
+
#### v-focus
|
|
430
|
+
|
|
431
|
+
Declaratively manage focus on an element. Focus is deferred via
|
|
432
|
+
`requestAnimationFrame`, so elements that become visible just before the
|
|
433
|
+
directive runs (for example inside a `v-if` or a `display: none` container)
|
|
434
|
+
still receive focus reliably.
|
|
435
|
+
|
|
436
|
+
```html
|
|
437
|
+
<!-- Focus once after mount -->
|
|
438
|
+
<input v-focus>
|
|
439
|
+
|
|
440
|
+
<!-- Focus + select all text after mount -->
|
|
441
|
+
<input v-focus.select>
|
|
442
|
+
|
|
443
|
+
<!-- Focus + place the caret at the end of the value -->
|
|
444
|
+
<input v-focus.cursor-end value="prefilled">
|
|
445
|
+
|
|
446
|
+
<!-- Conditional focus: fires when the expression goes from falsy to truthy -->
|
|
447
|
+
<input v-focus="isEditing">
|
|
448
|
+
|
|
449
|
+
<!-- Conditional focus + select all -->
|
|
450
|
+
<input v-focus.select="isEditing">
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Behavior:**
|
|
454
|
+
|
|
455
|
+
- **Without an expression**, the element is focused exactly once after mount.
|
|
456
|
+
- **With an expression**, focus fires only on the falsy → truthy edge, so the
|
|
457
|
+
user is not repeatedly re-focused on every reactive update. If the value is
|
|
458
|
+
already truthy on mount, the element is focused immediately.
|
|
459
|
+
|
|
460
|
+
**Modifiers:**
|
|
461
|
+
|
|
462
|
+
- `.select` - After focusing, selects all text in the input/textarea
|
|
463
|
+
- `.cursor-end` - After focusing, places the caret at the end of the value
|
|
464
|
+
|
|
317
465
|
#### v-resize
|
|
318
466
|
|
|
319
467
|
Monitor element size changes using ResizeObserver:
|
|
@@ -591,9 +739,20 @@ Two-way data binding:
|
|
|
591
739
|
<input v-model.number="age"> <!-- Convert to number -->
|
|
592
740
|
<input v-model.trim="username"> <!-- Trim whitespace -->
|
|
593
741
|
|
|
594
|
-
<!-- Checkbox -->
|
|
742
|
+
<!-- Checkbox (boolean) -->
|
|
595
743
|
<input type="checkbox" v-model="isChecked">
|
|
596
744
|
|
|
745
|
+
<!-- Checkbox with custom true/false values -->
|
|
746
|
+
<input type="checkbox" v-model="status" :true-value="'yes'" :false-value="'no'">
|
|
747
|
+
|
|
748
|
+
<!-- Checkbox group bound to an array -->
|
|
749
|
+
<input type="checkbox" value="a" v-model="selectedItems">
|
|
750
|
+
<input type="checkbox" value="b" v-model="selectedItems">
|
|
751
|
+
|
|
752
|
+
<!-- Radio -->
|
|
753
|
+
<input type="radio" value="a" v-model="picked">
|
|
754
|
+
<input type="radio" value="b" v-model="picked">
|
|
755
|
+
|
|
597
756
|
<!-- Select -->
|
|
598
757
|
<select v-model="selected">
|
|
599
758
|
<option value="a">Option A</option>
|
|
@@ -601,6 +760,13 @@ Two-way data binding:
|
|
|
601
760
|
</select>
|
|
602
761
|
```
|
|
603
762
|
|
|
763
|
+
**Supported elements:**
|
|
764
|
+
|
|
765
|
+
- **Text inputs / `<textarea>`** - Binds to the element's value
|
|
766
|
+
- **Checkbox** - Binds to a boolean, to a custom value pair via `:true-value` / `:false-value`, or to an array (when the bound value is an array, the checkbox's `value` is added/removed)
|
|
767
|
+
- **Radio** - Binds to the `value` (or `:value`) of the selected radio button
|
|
768
|
+
- **Select** - Binds to the selected option's value (re-applied automatically when options are generated dynamically via `v-for`)
|
|
769
|
+
|
|
604
770
|
### Methods
|
|
605
771
|
|
|
606
772
|
Methods have access to data and computed properties via `this`:
|
|
@@ -639,6 +805,108 @@ methods: {
|
|
|
639
805
|
}
|
|
640
806
|
```
|
|
641
807
|
|
|
808
|
+
## Components
|
|
809
|
+
|
|
810
|
+
ichigo.js components are real [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
|
|
811
|
+
backed by the same reactivity system. Define a component with `defineComponent`,
|
|
812
|
+
pointing it at a `<template>` for its markup.
|
|
813
|
+
|
|
814
|
+
```html
|
|
815
|
+
<!-- Component markup -->
|
|
816
|
+
<template id="my-list">
|
|
817
|
+
<ul v-if="items.length > 0">
|
|
818
|
+
<li v-for="item of items" :key="item.id">{{ item.name }}</li>
|
|
819
|
+
</ul>
|
|
820
|
+
<!-- Fallback content projected from the parent -->
|
|
821
|
+
<slot></slot>
|
|
822
|
+
</template>
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
```javascript
|
|
826
|
+
import { defineComponent } from '@mintjamsinc/ichigojs';
|
|
827
|
+
|
|
828
|
+
defineComponent('my-list', {
|
|
829
|
+
template: '#my-list', // CSS selector for the <template>
|
|
830
|
+
props: ['items'], // Props received from the parent
|
|
831
|
+
data() {
|
|
832
|
+
// Props are accessible via `this` and can be defaulted/transformed here
|
|
833
|
+
return { items: this.items ?? [] };
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
```html
|
|
839
|
+
<!-- Usage -->
|
|
840
|
+
<my-list :items="searchResults">
|
|
841
|
+
<span slot="empty">No results.</span>
|
|
842
|
+
</my-list>
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Props:**
|
|
846
|
+
|
|
847
|
+
- Declared via the `props` array. Each declared prop becomes a property on the
|
|
848
|
+
custom element, so the parent can bind to it with `v-bind` / `:`
|
|
849
|
+
(e.g. `:items="searchResults"`).
|
|
850
|
+
- Props are reactive from the start and are included in the component's data
|
|
851
|
+
automatically. Values returned from `data()` take precedence, allowing you to
|
|
852
|
+
default or transform a prop (e.g. `this.items ?? []`).
|
|
853
|
+
|
|
854
|
+
**Slots:**
|
|
855
|
+
|
|
856
|
+
Use the native `<slot>` element in the component template to project content
|
|
857
|
+
from the parent. ichigo.js components use Light DOM.
|
|
858
|
+
|
|
859
|
+
### Events (`$emit`)
|
|
860
|
+
|
|
861
|
+
Components (and applications) can dispatch custom events with `$emit`, which is
|
|
862
|
+
available in both templates and methods. By default the event bubbles from the
|
|
863
|
+
component's root element, so a parent can listen for it with `v-on` / `@` on the
|
|
864
|
+
component tag.
|
|
865
|
+
|
|
866
|
+
```javascript
|
|
867
|
+
defineComponent('my-button', {
|
|
868
|
+
template: '#my-button',
|
|
869
|
+
// Optional: declare the events this component emits.
|
|
870
|
+
// Emitting an undeclared event logs a development warning (validation only;
|
|
871
|
+
// it never blocks dispatch). Omit `emits` to allow any event name.
|
|
872
|
+
emits: ['selected'],
|
|
873
|
+
methods: {
|
|
874
|
+
onClick() {
|
|
875
|
+
// $emit(name, detail?, options?)
|
|
876
|
+
this.$emit('selected', { id: 42 });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
```html
|
|
883
|
+
<!-- Parent listens for the custom event; payload is in event.detail -->
|
|
884
|
+
<my-button @selected="onSelected"></my-button>
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
**`$emit(name, detail?, options?)`:**
|
|
888
|
+
|
|
889
|
+
- `name` - The event name (listened to as `@name` on the parent)
|
|
890
|
+
- `detail` - The payload exposed as `event.detail`
|
|
891
|
+
- `options` - Dispatch options (`VEmitOptions`):
|
|
892
|
+
- `bubbles` - Whether the event bubbles (default: `true`)
|
|
893
|
+
- `cancelable` - Whether `preventDefault()` has an effect (default: `true`); `$emit` returns `false` when a listener calls `preventDefault()`
|
|
894
|
+
- `composed` - Whether the event crosses shadow DOM boundaries (default: `false`)
|
|
895
|
+
- `target` - The dispatch target (default: the application root element). Set to `document` / `window` for a global event bus.
|
|
896
|
+
|
|
897
|
+
### Legacy component directive (`v-component`)
|
|
898
|
+
|
|
899
|
+
> ⚠️ **Deprecated.** The `v-component` directive and the `VComponentRegistry`
|
|
900
|
+
> are deprecated and will be removed in a future release. Use
|
|
901
|
+
> [`defineComponent`](#components) (Custom Elements) for new code.
|
|
902
|
+
|
|
903
|
+
For reference, the legacy mechanism renders a component registered in the
|
|
904
|
+
application's `VComponentRegistry` by id, passing props through `:options`:
|
|
905
|
+
|
|
906
|
+
```html
|
|
907
|
+
<div v-component="my-component" :options="{ message: 'Hello' }"></div>
|
|
908
|
+
```
|
|
909
|
+
|
|
642
910
|
## Performance
|
|
643
911
|
|
|
644
912
|
ichigo.js uses several optimization techniques:
|
|
@@ -696,13 +964,33 @@ Creates a new application instance.
|
|
|
696
964
|
|
|
697
965
|
**Options:**
|
|
698
966
|
|
|
699
|
-
- `data()`: Function that returns the initial data object
|
|
700
|
-
- `computed`: Object containing computed property definitions
|
|
967
|
+
- `data()`: Function that returns the initial data object. Called with a `$ctx` (`{ $markRaw }`) as `this`.
|
|
968
|
+
- `computed`: Object containing computed property definitions. Each value is either a getter function (read-only) or a `{ get, set }` object (writable).
|
|
701
969
|
- `methods`: Object containing method definitions
|
|
970
|
+
- `watch`: Object mapping property paths to watcher definitions (a callback, or `{ handler, deep, immediate }`)
|
|
971
|
+
- `emits`: Optional array of event names the app/component is expected to emit via `$emit`. Emitting an undeclared event logs a development warning (validation only).
|
|
702
972
|
- `logLevel`: Logging level (`'debug'` | `'info'` | `'warn'` | `'error'`)
|
|
703
973
|
|
|
704
974
|
**Returns:** Application instance with `mount(selector)` method
|
|
705
975
|
|
|
976
|
+
**Instance helpers** (available in `data()`, methods, expressions, and lifecycle hooks as appropriate):
|
|
977
|
+
|
|
978
|
+
- `$markRaw(obj)`: Marks an object as non-reactive (see [Marking Objects as Non-Reactive](#marking-objects-as-non-reactive))
|
|
979
|
+
- `$nextTick(callback)`: Runs a callback after the next DOM update
|
|
980
|
+
- `$emit(name, detail?, options?)`: Dispatches a custom event (see [Events](#events-emit))
|
|
981
|
+
- `$ctx`: Lifecycle/handler context with `element`, `vnode`, and `userData`
|
|
982
|
+
|
|
983
|
+
### defineComponent(tagName, options)
|
|
984
|
+
|
|
985
|
+
Defines and registers a custom element backed by ichigo.js reactivity. See [Components](#components).
|
|
986
|
+
|
|
987
|
+
**Options** (extends the `createApp` options above):
|
|
988
|
+
|
|
989
|
+
- `template`: CSS selector for the `<template>` element that defines the component's markup (required)
|
|
990
|
+
- `props`: Array of property names received from the parent via attribute/property binding
|
|
991
|
+
|
|
992
|
+
**Returns:** `void` (the custom element is registered via `customElements.define`)
|
|
993
|
+
|
|
706
994
|
## Contributing
|
|
707
995
|
|
|
708
996
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/ichigo.cjs
CHANGED
|
@@ -10270,9 +10270,18 @@
|
|
|
10270
10270
|
#sourceName;
|
|
10271
10271
|
#useOfSyntax = false; // Track if 'of' syntax was used
|
|
10272
10272
|
/**
|
|
10273
|
-
*
|
|
10273
|
+
* Ordered list of currently rendered items.
|
|
10274
|
+
*
|
|
10275
|
+
* This is intentionally an ordered array of { key, vNode } entries rather
|
|
10276
|
+
* than a Map<key, VNode>. The :key attribute is a *reconciliation hint*
|
|
10277
|
+
* used to identify and reuse the same logical row across re-renders — it is
|
|
10278
|
+
* not the identity of the rendered set. Keying the rendered set by a Map
|
|
10279
|
+
* would make it structurally impossible to hold two rows that resolve to
|
|
10280
|
+
* the same key, which would silently drop application data. The most
|
|
10281
|
+
* fundamental invariant of a list directive is "N items in => N rows out",
|
|
10282
|
+
* so the rendered set must be a positional list that can carry duplicates.
|
|
10274
10283
|
*/
|
|
10275
|
-
#renderedItems =
|
|
10284
|
+
#renderedItems = [];
|
|
10276
10285
|
/**
|
|
10277
10286
|
* Previous iterations to detect changes
|
|
10278
10287
|
*/
|
|
@@ -10395,11 +10404,11 @@
|
|
|
10395
10404
|
destroy() {
|
|
10396
10405
|
// Clean up all rendered items
|
|
10397
10406
|
// First destroy all VNodes (calls @unmount hooks), then remove from DOM
|
|
10398
|
-
for (const vNode of this.#renderedItems
|
|
10407
|
+
for (const { vNode } of this.#renderedItems) {
|
|
10399
10408
|
vNode.destroy();
|
|
10400
10409
|
}
|
|
10401
10410
|
// Then remove DOM nodes
|
|
10402
|
-
for (const vNode of this.#renderedItems
|
|
10411
|
+
for (const { vNode } of this.#renderedItems) {
|
|
10403
10412
|
const range = vNode.fragmentRange;
|
|
10404
10413
|
if (range) {
|
|
10405
10414
|
range.remove();
|
|
@@ -10410,7 +10419,7 @@
|
|
|
10410
10419
|
vNode.node.parentNode.removeChild(vNode.node);
|
|
10411
10420
|
}
|
|
10412
10421
|
}
|
|
10413
|
-
this.#renderedItems
|
|
10422
|
+
this.#renderedItems = [];
|
|
10414
10423
|
this.#previousIterations = [];
|
|
10415
10424
|
}
|
|
10416
10425
|
/**
|
|
@@ -10451,7 +10460,24 @@
|
|
|
10451
10460
|
this.#previousIterations = iterations;
|
|
10452
10461
|
}
|
|
10453
10462
|
/**
|
|
10454
|
-
* Key-based diffing for efficient DOM updates
|
|
10463
|
+
* Key-based diffing for efficient DOM updates.
|
|
10464
|
+
*
|
|
10465
|
+
* Reconciliation model
|
|
10466
|
+
* --------------------
|
|
10467
|
+
* The :key attribute is treated as a *hint* for reusing the same logical
|
|
10468
|
+
* row across re-renders, not as the identity of the rendered set. The
|
|
10469
|
+
* previously rendered rows are placed into a pool keyed by :key (a queue
|
|
10470
|
+
* per key, so equal keys can hold more than one row). Each incoming
|
|
10471
|
+
* iteration then claims a row from its key's queue when one is available,
|
|
10472
|
+
* otherwise a fresh row is created. Whatever stays in the pool at the end
|
|
10473
|
+
* is genuinely gone and is destroyed and removed.
|
|
10474
|
+
*
|
|
10475
|
+
* This guarantees the directive's most fundamental invariant — "N items in
|
|
10476
|
+
* => N rows out" — even when the application supplies duplicate keys. We
|
|
10477
|
+
* still warn on duplicates because they make reuse ambiguous (reordering
|
|
10478
|
+
* becomes positional rather than identity-stable), but we never silently
|
|
10479
|
+
* drop the application's data, and no row is ever orphaned: every old row
|
|
10480
|
+
* is either reused or explicitly removed.
|
|
10455
10481
|
*/
|
|
10456
10482
|
#updateList(newIterations) {
|
|
10457
10483
|
const parent = this.#vNode.anchorNode?.parentNode;
|
|
@@ -10459,22 +10485,38 @@
|
|
|
10459
10485
|
if (!parent || !anchor) {
|
|
10460
10486
|
throw new Error('v-for element must have a parent and anchor');
|
|
10461
10487
|
}
|
|
10462
|
-
|
|
10463
|
-
//
|
|
10464
|
-
|
|
10488
|
+
// Build a reuse pool from the currently rendered rows. A queue per key
|
|
10489
|
+
// (FIFO) lets duplicate keys reuse multiple rows: the first incoming
|
|
10490
|
+
// occurrence claims the first existing row, the second claims the next,
|
|
10491
|
+
// and so on.
|
|
10492
|
+
const pool = new Map();
|
|
10493
|
+
for (const { key, vNode } of this.#renderedItems) {
|
|
10494
|
+
let queue = pool.get(key);
|
|
10495
|
+
if (!queue) {
|
|
10496
|
+
queue = [];
|
|
10497
|
+
pool.set(key, queue);
|
|
10498
|
+
}
|
|
10499
|
+
queue.push(vNode);
|
|
10500
|
+
}
|
|
10501
|
+
// Decide, for each incoming iteration in order, whether it reuses an
|
|
10502
|
+
// existing row or needs a new one. Reused rows are taken out of the
|
|
10503
|
+
// pool so that what remains afterwards is exactly the set to remove.
|
|
10465
10504
|
const seenKeys = new Set();
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10505
|
+
const plan = [];
|
|
10506
|
+
for (const context of newIterations) {
|
|
10507
|
+
if (seenKeys.has(context.key)) {
|
|
10508
|
+
console.warn(`[ichigo.js] Duplicate key detected in v-for: "${context.key}". All entries are still rendered, but reordering may be unstable. Keys should be unique.`);
|
|
10469
10509
|
}
|
|
10470
|
-
seenKeys.add(
|
|
10471
|
-
|
|
10510
|
+
seenKeys.add(context.key);
|
|
10511
|
+
const queue = pool.get(context.key);
|
|
10512
|
+
const reused = queue && queue.length ? queue.shift() : undefined;
|
|
10513
|
+
plan.push({ context, reused });
|
|
10472
10514
|
}
|
|
10473
|
-
// Remove
|
|
10515
|
+
// Remove rows that were not reused.
|
|
10474
10516
|
// First destroy VNodes (calls @unmount hooks while DOM is still accessible)
|
|
10475
10517
|
const nodesToRemove = [];
|
|
10476
|
-
for (const
|
|
10477
|
-
|
|
10518
|
+
for (const queue of pool.values()) {
|
|
10519
|
+
for (const vNode of queue) {
|
|
10478
10520
|
nodesToRemove.push(vNode);
|
|
10479
10521
|
vNode.destroy();
|
|
10480
10522
|
}
|
|
@@ -10493,11 +10535,12 @@
|
|
|
10493
10535
|
parentOfNode.removeChild(vNode.node);
|
|
10494
10536
|
}
|
|
10495
10537
|
}
|
|
10496
|
-
// Add or reorder
|
|
10538
|
+
// Add or reorder rows, building the new ordered rendered set.
|
|
10539
|
+
const newRenderedItems = [];
|
|
10497
10540
|
let prevNode = anchor;
|
|
10498
|
-
for (const context of
|
|
10541
|
+
for (const { context, reused } of plan) {
|
|
10499
10542
|
const { key } = context;
|
|
10500
|
-
let vNode =
|
|
10543
|
+
let vNode = reused;
|
|
10501
10544
|
if (!vNode) {
|
|
10502
10545
|
// Create new item
|
|
10503
10546
|
const clone = this.#cloneNode();
|
|
@@ -10529,7 +10572,7 @@
|
|
|
10529
10572
|
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
10530
10573
|
const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
|
|
10531
10574
|
vNode.fragmentRange = range;
|
|
10532
|
-
newRenderedItems.
|
|
10575
|
+
newRenderedItems.push({ key, vNode });
|
|
10533
10576
|
vNode.forceUpdate();
|
|
10534
10577
|
prevNode = range.lastNode;
|
|
10535
10578
|
continue;
|
|
@@ -10543,12 +10586,12 @@
|
|
|
10543
10586
|
else {
|
|
10544
10587
|
parent.appendChild(nodeToInsert);
|
|
10545
10588
|
}
|
|
10546
|
-
newRenderedItems.
|
|
10589
|
+
newRenderedItems.push({ key, vNode });
|
|
10547
10590
|
vNode.forceUpdate();
|
|
10548
10591
|
}
|
|
10549
10592
|
else {
|
|
10550
10593
|
// Reuse existing item
|
|
10551
|
-
newRenderedItems.
|
|
10594
|
+
newRenderedItems.push({ key, vNode });
|
|
10552
10595
|
// Update bindings
|
|
10553
10596
|
this.#updateItemBindings(vNode, context);
|
|
10554
10597
|
// For fragment-backed iterations, move the entire range atomically.
|
|
@@ -10574,7 +10617,7 @@
|
|
|
10574
10617
|
// Advance prevNode to this iteration's last DOM node
|
|
10575
10618
|
prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
|
|
10576
10619
|
}
|
|
10577
|
-
// Update rendered
|
|
10620
|
+
// Update the ordered rendered set
|
|
10578
10621
|
this.#renderedItems = newRenderedItems;
|
|
10579
10622
|
}
|
|
10580
10623
|
/**
|