@mintjamsinc/ichigojs 0.1.53 → 0.1.55
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/ichigo.cjs +480 -29
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +479 -30
- 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 +480 -29
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VDOM.d.ts +1 -0
- package/dist/types/ichigo/components/IchigoComponentOptions.d.ts +19 -0
- package/dist/types/ichigo/components/IchigoElement.d.ts +43 -0
- package/dist/types/ichigo/components/VComponent.d.ts +1 -0
- package/dist/types/ichigo/components/VComponentRegistry.d.ts +1 -0
- package/dist/types/ichigo/components/defineComponent.d.ts +34 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +1 -1
package/dist/ichigo.cjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8
8
|
/**
|
|
9
9
|
* Represents a reusable component definition.
|
|
10
|
+
* @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
10
11
|
*/
|
|
11
12
|
class VComponent {
|
|
12
13
|
/**
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
45
46
|
/**
|
|
46
47
|
* A registry for managing component definitions.
|
|
48
|
+
* @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
47
49
|
*/
|
|
48
50
|
class VComponentRegistry {
|
|
49
51
|
/**
|
|
@@ -7474,6 +7476,14 @@
|
|
|
7474
7476
|
else if (attributeName === 'style') {
|
|
7475
7477
|
this.#updateStyle(element, value);
|
|
7476
7478
|
}
|
|
7479
|
+
else if (this.#isCustomElementProperty(element, attributeName, value)) {
|
|
7480
|
+
// Custom elements: set objects/arrays (and declared props) as properties
|
|
7481
|
+
// so complex values are not serialized to strings via setAttribute.
|
|
7482
|
+
// HTML attributes are lowercased by the browser, so resolve to the
|
|
7483
|
+
// actual camelCase property name declared on the custom element.
|
|
7484
|
+
const propName = this.#resolveCustomElementPropertyName(element, attributeName);
|
|
7485
|
+
element[propName] = value;
|
|
7486
|
+
}
|
|
7477
7487
|
else if (this.#isDOMProperty(attributeName)) {
|
|
7478
7488
|
this.#updateProperty(element, attributeName, value);
|
|
7479
7489
|
}
|
|
@@ -7557,6 +7567,46 @@
|
|
|
7557
7567
|
#camelToKebab(str) {
|
|
7558
7568
|
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
7559
7569
|
}
|
|
7570
|
+
/**
|
|
7571
|
+
* Returns true when the target is a custom element and the binding should be
|
|
7572
|
+
* delivered as a property rather than an HTML attribute.
|
|
7573
|
+
*
|
|
7574
|
+
* Two conditions trigger property delivery:
|
|
7575
|
+
* 1. The value is an object or array — serialising these to a string attribute
|
|
7576
|
+
* would lose type information.
|
|
7577
|
+
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
7578
|
+
* defineComponent), even when the value is a primitive.
|
|
7579
|
+
*/
|
|
7580
|
+
#isCustomElementProperty(element, name, value) {
|
|
7581
|
+
if (!element.tagName.includes('-')) {
|
|
7582
|
+
return false;
|
|
7583
|
+
}
|
|
7584
|
+
const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
7585
|
+
return isObjectOrArray || name in element;
|
|
7586
|
+
}
|
|
7587
|
+
/**
|
|
7588
|
+
* Resolves the actual property name on a custom element for a given
|
|
7589
|
+
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
7590
|
+
* but custom element props are typically camelCase. This method checks the
|
|
7591
|
+
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
7592
|
+
* match, falling back to scanning the prototype's own property descriptors.
|
|
7593
|
+
*/
|
|
7594
|
+
#resolveCustomElementPropertyName(element, name) {
|
|
7595
|
+
// Fast path: exact match already exists
|
|
7596
|
+
if (name in element) {
|
|
7597
|
+
return name;
|
|
7598
|
+
}
|
|
7599
|
+
// Check declared _props from defineComponent / IchigoElement
|
|
7600
|
+
const props = element.constructor._props;
|
|
7601
|
+
if (Array.isArray(props)) {
|
|
7602
|
+
const lowerName = name.toLowerCase();
|
|
7603
|
+
const match = props.find(p => p.toLowerCase() === lowerName);
|
|
7604
|
+
if (match) {
|
|
7605
|
+
return match;
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
return name;
|
|
7609
|
+
}
|
|
7560
7610
|
/**
|
|
7561
7611
|
* Checks if the attribute should be set as a DOM property.
|
|
7562
7612
|
*/
|
|
@@ -7910,22 +7960,42 @@
|
|
|
7910
7960
|
const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
|
7911
7961
|
return ReactiveProxy.create(value, onChange, nestedPath);
|
|
7912
7962
|
}
|
|
7913
|
-
//
|
|
7963
|
+
// If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
|
|
7914
7964
|
if (typeof value === 'function') {
|
|
7915
|
-
|
|
7965
|
+
// For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
|
|
7916
7966
|
if (Array.isArray(obj)) {
|
|
7917
|
-
|
|
7967
|
+
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
7968
|
+
if (!arrayMutationMethods.includes(key)) {
|
|
7969
|
+
return value;
|
|
7970
|
+
}
|
|
7971
|
+
return function (...args) {
|
|
7972
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
7973
|
+
onChange(path || undefined);
|
|
7974
|
+
return result;
|
|
7975
|
+
};
|
|
7918
7976
|
}
|
|
7919
|
-
|
|
7920
|
-
|
|
7977
|
+
// For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
|
|
7978
|
+
if (obj.constructor.name === 'Map') {
|
|
7979
|
+
const mapMutationMethods = ['set', 'delete', 'clear'];
|
|
7980
|
+
return function (...args) {
|
|
7981
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
7982
|
+
if (mapMutationMethods.includes(key)) {
|
|
7983
|
+
onChange(path || undefined);
|
|
7984
|
+
}
|
|
7985
|
+
return result;
|
|
7986
|
+
};
|
|
7987
|
+
}
|
|
7988
|
+
// For Set, we only want to wrap mutation methods, not read methods like 'has'
|
|
7989
|
+
if (obj.constructor.name === 'Set') {
|
|
7990
|
+
const setMutationMethods = ['add', 'delete', 'clear'];
|
|
7991
|
+
return function (...args) {
|
|
7992
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
7993
|
+
if (setMutationMethods.includes(key)) {
|
|
7994
|
+
onChange(path || undefined);
|
|
7995
|
+
}
|
|
7996
|
+
return result;
|
|
7997
|
+
};
|
|
7921
7998
|
}
|
|
7922
|
-
return function (...args) {
|
|
7923
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
7924
|
-
if (mutationMethods.includes(key)) {
|
|
7925
|
-
onChange(path || undefined);
|
|
7926
|
-
}
|
|
7927
|
-
return result;
|
|
7928
|
-
};
|
|
7929
7999
|
}
|
|
7930
8000
|
return value;
|
|
7931
8001
|
},
|
|
@@ -8917,6 +8987,18 @@
|
|
|
8917
8987
|
});
|
|
8918
8988
|
}
|
|
8919
8989
|
}
|
|
8990
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
8991
|
+
// If the node is a DocumentFragment, create child VNodes for its children
|
|
8992
|
+
// DocumentFragment is not an Element so it has no directives itself.
|
|
8993
|
+
this.#childVNodes = [];
|
|
8994
|
+
for (const childNode of Array.from(this.#node.childNodes)) {
|
|
8995
|
+
new VNode({
|
|
8996
|
+
node: childNode,
|
|
8997
|
+
vApplication: this.#vApplication,
|
|
8998
|
+
parentVNode: this
|
|
8999
|
+
});
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
8920
9002
|
// Register this node as a dependent of the parent node, if any
|
|
8921
9003
|
this.#closers = this.#parentVNode?.addDependent(this);
|
|
8922
9004
|
}
|
|
@@ -9171,6 +9253,16 @@
|
|
|
9171
9253
|
});
|
|
9172
9254
|
}
|
|
9173
9255
|
}
|
|
9256
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9257
|
+
// For document fragments (e.g. from <template v-if>), propagate updates
|
|
9258
|
+
// to dependent virtual nodes so that child bindings stay reactive.
|
|
9259
|
+
this.#dependents?.forEach(dependentNode => {
|
|
9260
|
+
const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
|
|
9261
|
+
if (changed) {
|
|
9262
|
+
dependentNode.update();
|
|
9263
|
+
}
|
|
9264
|
+
});
|
|
9265
|
+
}
|
|
9174
9266
|
}
|
|
9175
9267
|
/**
|
|
9176
9268
|
* Forces an update of the virtual node and its children, regardless of changed identifiers.
|
|
@@ -9220,6 +9312,12 @@
|
|
|
9220
9312
|
});
|
|
9221
9313
|
}
|
|
9222
9314
|
}
|
|
9315
|
+
else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
9316
|
+
// For document fragments, recursively force update child VNodes
|
|
9317
|
+
this.#childVNodes?.forEach(childVNode => {
|
|
9318
|
+
childVNode.forceUpdate();
|
|
9319
|
+
});
|
|
9320
|
+
}
|
|
9223
9321
|
}
|
|
9224
9322
|
/**
|
|
9225
9323
|
* Adds a child virtual node to this virtual node.
|
|
@@ -9597,9 +9695,11 @@
|
|
|
9597
9695
|
}
|
|
9598
9696
|
// Clone the original node and create a new VNode for it
|
|
9599
9697
|
const clone = this.#cloneNode();
|
|
9600
|
-
//
|
|
9601
|
-
|
|
9602
|
-
//
|
|
9698
|
+
// Create a new VNode for the cloned element BEFORE inserting into DOM.
|
|
9699
|
+
// This prevents custom elements (Web Components) from having their
|
|
9700
|
+
// connectedCallback fire before VNode processes their children, which
|
|
9701
|
+
// would cause the parent VApplication to incorrectly adopt the custom
|
|
9702
|
+
// element's internal template content as its own VNode tree.
|
|
9603
9703
|
// Pass the current bindings to ensure loop variables from v-for are available
|
|
9604
9704
|
const vNode = new VNode({
|
|
9605
9705
|
node: clone,
|
|
@@ -9607,6 +9707,29 @@
|
|
|
9607
9707
|
parentVNode: this.#vNode,
|
|
9608
9708
|
bindings: this.#vNode.bindings
|
|
9609
9709
|
});
|
|
9710
|
+
// Insert after the anchor node AFTER VNode creation
|
|
9711
|
+
const anchorParent = this.#vNode.anchorNode?.parentNode;
|
|
9712
|
+
const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
|
|
9713
|
+
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
|
|
9714
|
+
const startMarker = document.createComment('#vif-fragment-start');
|
|
9715
|
+
const endMarker = document.createComment('#vif-fragment-end');
|
|
9716
|
+
if (nextSibling) {
|
|
9717
|
+
anchorParent.insertBefore(startMarker, nextSibling);
|
|
9718
|
+
}
|
|
9719
|
+
else {
|
|
9720
|
+
anchorParent.appendChild(startMarker);
|
|
9721
|
+
}
|
|
9722
|
+
anchorParent.insertBefore(endMarker, startMarker.nextSibling);
|
|
9723
|
+
anchorParent.insertBefore(clone, endMarker);
|
|
9724
|
+
// Store markers for later removal
|
|
9725
|
+
vNode.userData.set('vif_fragment_start', startMarker);
|
|
9726
|
+
vNode.userData.set('vif_fragment_end', endMarker);
|
|
9727
|
+
this.#renderedVNode = vNode;
|
|
9728
|
+
this.#renderedVNode.forceUpdate();
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9731
|
+
const nodeToInsert = vNode.anchorNode || clone;
|
|
9732
|
+
anchorParent?.insertBefore(nodeToInsert, nextSibling);
|
|
9610
9733
|
this.#renderedVNode = vNode;
|
|
9611
9734
|
this.#renderedVNode.forceUpdate();
|
|
9612
9735
|
}
|
|
@@ -9621,17 +9744,40 @@
|
|
|
9621
9744
|
}
|
|
9622
9745
|
// Destroy VNode first (calls @unmount hooks while DOM is still accessible)
|
|
9623
9746
|
this.#renderedVNode.destroy();
|
|
9624
|
-
// Then remove from DOM
|
|
9625
|
-
this.#renderedVNode.
|
|
9747
|
+
// Then remove from DOM. Handle fragment markers if present
|
|
9748
|
+
const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
|
|
9749
|
+
const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
|
|
9750
|
+
if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
|
|
9751
|
+
const parentNode = startMarker.parentNode;
|
|
9752
|
+
let node = startMarker;
|
|
9753
|
+
while (node) {
|
|
9754
|
+
const next = node.nextSibling;
|
|
9755
|
+
parentNode.removeChild(node);
|
|
9756
|
+
if (node === endMarker)
|
|
9757
|
+
break;
|
|
9758
|
+
node = next;
|
|
9759
|
+
}
|
|
9760
|
+
this.#renderedVNode = undefined;
|
|
9761
|
+
return;
|
|
9762
|
+
}
|
|
9763
|
+
const parent = this.#renderedVNode.node.parentNode;
|
|
9764
|
+
if (parent) {
|
|
9765
|
+
parent.removeChild(this.#renderedVNode.node);
|
|
9766
|
+
}
|
|
9626
9767
|
this.#renderedVNode = undefined;
|
|
9627
9768
|
}
|
|
9628
9769
|
/**
|
|
9629
9770
|
* Clones the original node of the directive's virtual node.
|
|
9630
|
-
*
|
|
9631
|
-
*
|
|
9771
|
+
* When the source element is a <template>, returns a DocumentFragment
|
|
9772
|
+
* cloned from the template's content.
|
|
9773
|
+
* @returns The cloned Node (HTMLElement or DocumentFragment).
|
|
9632
9774
|
*/
|
|
9633
9775
|
#cloneNode() {
|
|
9634
9776
|
const element = this.#vNode.node;
|
|
9777
|
+
if (element instanceof HTMLTemplateElement) {
|
|
9778
|
+
// Return a DocumentFragment cloned from the template content
|
|
9779
|
+
return element.content.cloneNode(true);
|
|
9780
|
+
}
|
|
9635
9781
|
return element.cloneNode(true);
|
|
9636
9782
|
}
|
|
9637
9783
|
/**
|
|
@@ -9949,10 +10095,28 @@
|
|
|
9949
10095
|
vNode.destroy();
|
|
9950
10096
|
}
|
|
9951
10097
|
}
|
|
9952
|
-
// Then remove from DOM
|
|
10098
|
+
// Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
|
|
9953
10099
|
for (const vNode of nodesToRemove) {
|
|
9954
|
-
|
|
9955
|
-
|
|
10100
|
+
// If this VNode stored fragment markers, remove the range between them
|
|
10101
|
+
const startMarker = vNode.userData.get?.('vfor_fragment_start');
|
|
10102
|
+
const endMarker = vNode.userData.get?.('vfor_fragment_end');
|
|
10103
|
+
if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
|
|
10104
|
+
const parentNode = startMarker.parentNode;
|
|
10105
|
+
let node = startMarker;
|
|
10106
|
+
// Remove nodes from startMarker up to and including endMarker
|
|
10107
|
+
while (node) {
|
|
10108
|
+
const next = node.nextSibling;
|
|
10109
|
+
parentNode.removeChild(node);
|
|
10110
|
+
if (node === endMarker)
|
|
10111
|
+
break;
|
|
10112
|
+
node = next;
|
|
10113
|
+
}
|
|
10114
|
+
continue;
|
|
10115
|
+
}
|
|
10116
|
+
// Fallback: remove the node itself if it's attached
|
|
10117
|
+
const parentOfNode = vNode.node.parentNode;
|
|
10118
|
+
if (parentOfNode) {
|
|
10119
|
+
parentOfNode.removeChild(vNode.node);
|
|
9956
10120
|
}
|
|
9957
10121
|
}
|
|
9958
10122
|
// Add or reorder items
|
|
@@ -9986,6 +10150,28 @@
|
|
|
9986
10150
|
bindings,
|
|
9987
10151
|
dependentIdentifiers: depIds,
|
|
9988
10152
|
});
|
|
10153
|
+
// If clone is a DocumentFragment, insert it between start/end comment markers
|
|
10154
|
+
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
10155
|
+
const startMarker = document.createComment('#vfor-fragment-start');
|
|
10156
|
+
const endMarker = document.createComment('#vfor-fragment-end');
|
|
10157
|
+
// Insert start and end markers and the fragment's children between them
|
|
10158
|
+
if (prevNode.nextSibling) {
|
|
10159
|
+
parent.insertBefore(startMarker, prevNode.nextSibling);
|
|
10160
|
+
}
|
|
10161
|
+
else {
|
|
10162
|
+
parent.appendChild(startMarker);
|
|
10163
|
+
}
|
|
10164
|
+
parent.insertBefore(endMarker, startMarker.nextSibling);
|
|
10165
|
+
parent.insertBefore(clone, endMarker);
|
|
10166
|
+
// Store markers on the VNode for later removal/movement
|
|
10167
|
+
vNode.userData.set('vfor_fragment_start', startMarker);
|
|
10168
|
+
vNode.userData.set('vfor_fragment_end', endMarker);
|
|
10169
|
+
newRenderedItems.set(key, vNode);
|
|
10170
|
+
vNode.forceUpdate();
|
|
10171
|
+
// Use endMarker as prevNode for subsequent insertions
|
|
10172
|
+
prevNode = endMarker;
|
|
10173
|
+
continue;
|
|
10174
|
+
}
|
|
9989
10175
|
// Determine what to insert: anchor node (if exists) or the clone itself
|
|
9990
10176
|
const nodeToInsert = vNode.anchorNode || clone;
|
|
9991
10177
|
// Insert after previous node
|
|
@@ -10003,8 +10189,9 @@
|
|
|
10003
10189
|
newRenderedItems.set(key, vNode);
|
|
10004
10190
|
// Update bindings
|
|
10005
10191
|
this.#updateItemBindings(vNode, context);
|
|
10006
|
-
// Determine the actual node in DOM: anchor node
|
|
10007
|
-
const
|
|
10192
|
+
// Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
|
|
10193
|
+
const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
|
|
10194
|
+
const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
|
|
10008
10195
|
// Move to correct position if needed
|
|
10009
10196
|
if (prevNode.nextSibling !== actualNode) {
|
|
10010
10197
|
if (prevNode.nextSibling) {
|
|
@@ -10015,8 +10202,9 @@
|
|
|
10015
10202
|
}
|
|
10016
10203
|
}
|
|
10017
10204
|
}
|
|
10018
|
-
// Use
|
|
10019
|
-
|
|
10205
|
+
// Use fragment end marker > anchor node > vNode.node as prevNode
|
|
10206
|
+
const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
|
|
10207
|
+
prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
|
|
10020
10208
|
}
|
|
10021
10209
|
// Update rendered items map
|
|
10022
10210
|
this.#renderedItems = newRenderedItems;
|
|
@@ -10195,12 +10383,17 @@
|
|
|
10195
10383
|
}
|
|
10196
10384
|
/**
|
|
10197
10385
|
* Clones the original node of the directive's virtual node.
|
|
10198
|
-
*
|
|
10199
|
-
*
|
|
10386
|
+
* When the source element is a <template>, its children (stored in .content)
|
|
10387
|
+
* are cloned into a DocumentFragment so that multiple root nodes can be
|
|
10388
|
+
* managed without adding an extra wrapper element.
|
|
10389
|
+
* @returns The cloned Node (Element or DocumentFragment).
|
|
10200
10390
|
*/
|
|
10201
10391
|
#cloneNode() {
|
|
10202
|
-
// Clone the original element
|
|
10203
10392
|
const element = this.#vNode.node;
|
|
10393
|
+
if (element instanceof HTMLTemplateElement) {
|
|
10394
|
+
// Return a cloned DocumentFragment containing the template content
|
|
10395
|
+
return element.content.cloneNode(true);
|
|
10396
|
+
}
|
|
10204
10397
|
return element.cloneNode(true);
|
|
10205
10398
|
}
|
|
10206
10399
|
/**
|
|
@@ -13006,6 +13199,7 @@
|
|
|
13006
13199
|
/**
|
|
13007
13200
|
* Gets the component registry.
|
|
13008
13201
|
* @return {VComponentRegistry} The component registry.
|
|
13202
|
+
* @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
|
|
13009
13203
|
*/
|
|
13010
13204
|
static get componentRegistry() {
|
|
13011
13205
|
return this.#componentRegistry;
|
|
@@ -13049,11 +13243,268 @@
|
|
|
13049
13243
|
}
|
|
13050
13244
|
}
|
|
13051
13245
|
|
|
13246
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
13247
|
+
/**
|
|
13248
|
+
* Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
|
|
13249
|
+
*
|
|
13250
|
+
* Mount timing:
|
|
13251
|
+
* - If the component declares no props, the VApplication is mounted synchronously
|
|
13252
|
+
* at the end of connectedCallback (the template is known, no props to wait for).
|
|
13253
|
+
* - If the component declares props, the VApplication is mounted the first time
|
|
13254
|
+
* _setProp() is called after connectedCallback has prepared the DOM. This
|
|
13255
|
+
* guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
|
|
13256
|
+
* already delivered at least one prop value before data() is evaluated.
|
|
13257
|
+
*
|
|
13258
|
+
* Subclasses must set the static fields _template, _props and _buildOptions before
|
|
13259
|
+
* calling customElements.define(). defineComponent() handles this automatically.
|
|
13260
|
+
*/
|
|
13261
|
+
class IchigoElement extends HTMLElement {
|
|
13262
|
+
/**
|
|
13263
|
+
* The mounted VApplication instance, present only while connected to the DOM.
|
|
13264
|
+
*/
|
|
13265
|
+
#app;
|
|
13266
|
+
/**
|
|
13267
|
+
* Stores prop values received at any time (before or after mount).
|
|
13268
|
+
*/
|
|
13269
|
+
#propValues = {};
|
|
13270
|
+
/**
|
|
13271
|
+
* The root element cloned from the template, ready for mounting.
|
|
13272
|
+
* Set by connectedCallback; cleared on disconnect.
|
|
13273
|
+
*/
|
|
13274
|
+
#mountRoot;
|
|
13275
|
+
/**
|
|
13276
|
+
* Whether a mount microtask is already queued to avoid double-mounting.
|
|
13277
|
+
*/
|
|
13278
|
+
#mountScheduled = false;
|
|
13279
|
+
connectedCallback() {
|
|
13280
|
+
// --- 0. Guard: skip if template not yet loaded ---
|
|
13281
|
+
// customElements.define() may run before loadComponent() completes (e.g. when
|
|
13282
|
+
// dynamic imports are inlined by the bundler). The static placeholder element
|
|
13283
|
+
// in the HTML will be removed and replaced by v-for once the template is ready,
|
|
13284
|
+
// so it is safe to do nothing here.
|
|
13285
|
+
const ctor = this.constructor;
|
|
13286
|
+
const templateEl = document.querySelector(ctor._template);
|
|
13287
|
+
if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
|
|
13288
|
+
return;
|
|
13289
|
+
}
|
|
13290
|
+
// --- 1. Capture slot content before clearing children ---
|
|
13291
|
+
const defaultSlotNodes = [];
|
|
13292
|
+
const namedSlotNodes = new Map();
|
|
13293
|
+
for (const child of Array.from(this.childNodes)) {
|
|
13294
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
13295
|
+
const el = child;
|
|
13296
|
+
const slotName = el.getAttribute('slot');
|
|
13297
|
+
if (slotName) {
|
|
13298
|
+
if (!namedSlotNodes.has(slotName)) {
|
|
13299
|
+
namedSlotNodes.set(slotName, []);
|
|
13300
|
+
}
|
|
13301
|
+
namedSlotNodes.get(slotName).push(el);
|
|
13302
|
+
// Remove slot attribute so ichigo.js doesn't try to bind it
|
|
13303
|
+
el.removeAttribute('slot');
|
|
13304
|
+
}
|
|
13305
|
+
else {
|
|
13306
|
+
defaultSlotNodes.push(el);
|
|
13307
|
+
}
|
|
13308
|
+
}
|
|
13309
|
+
else if (child.nodeType === Node.TEXT_NODE) {
|
|
13310
|
+
if ((child.textContent ?? '').trim()) {
|
|
13311
|
+
defaultSlotNodes.push(child);
|
|
13312
|
+
}
|
|
13313
|
+
}
|
|
13314
|
+
}
|
|
13315
|
+
// Clear host element so we can append the cloned template
|
|
13316
|
+
while (this.firstChild) {
|
|
13317
|
+
this.removeChild(this.firstChild);
|
|
13318
|
+
}
|
|
13319
|
+
// --- 2. Clone the component template ---
|
|
13320
|
+
const fragment = templateEl.content.cloneNode(true);
|
|
13321
|
+
const root = this.#findRootElement(fragment);
|
|
13322
|
+
// --- 3. Distribute named slot content ---
|
|
13323
|
+
for (const [name, nodes] of namedSlotNodes) {
|
|
13324
|
+
const slot = root.querySelector(`slot[name="${name}"]`);
|
|
13325
|
+
if (slot) {
|
|
13326
|
+
slot.replaceWith(...nodes);
|
|
13327
|
+
}
|
|
13328
|
+
}
|
|
13329
|
+
// --- 4. Distribute default slot content ---
|
|
13330
|
+
const defaultSlot = root.querySelector('slot:not([name])');
|
|
13331
|
+
if (defaultSlot && defaultSlotNodes.length > 0) {
|
|
13332
|
+
defaultSlot.replaceWith(...defaultSlotNodes);
|
|
13333
|
+
}
|
|
13334
|
+
// Attach the populated template to the host element
|
|
13335
|
+
this.appendChild(root);
|
|
13336
|
+
this.#mountRoot = root;
|
|
13337
|
+
// If props were set before connectedCallback, ensure mount is scheduled
|
|
13338
|
+
this.#scheduleMountIfNeeded();
|
|
13339
|
+
// --- 5. Mount strategy ---
|
|
13340
|
+
// If this component has no declared props, mount immediately.
|
|
13341
|
+
// If it has props, mount will be triggered from _setProp() once the parent
|
|
13342
|
+
// delivers the first prop value (via VBindDirective / forceUpdate).
|
|
13343
|
+
if (ctor._props.length === 0) {
|
|
13344
|
+
this.#doMount();
|
|
13345
|
+
}
|
|
13346
|
+
// else: wait for _setProp() to trigger #scheduleMountIfNeeded()
|
|
13347
|
+
}
|
|
13348
|
+
disconnectedCallback() {
|
|
13349
|
+
this.#mountRoot = undefined;
|
|
13350
|
+
this.#mountScheduled = false;
|
|
13351
|
+
if (this.#app) {
|
|
13352
|
+
this.#app.unmount();
|
|
13353
|
+
this.#app = undefined;
|
|
13354
|
+
}
|
|
13355
|
+
}
|
|
13356
|
+
/**
|
|
13357
|
+
* Called by the property setters generated by defineComponent().
|
|
13358
|
+
* Before mount: stores the value and schedules a mount microtask.
|
|
13359
|
+
* After mount: pushes the value directly into the reactive bindings.
|
|
13360
|
+
*/
|
|
13361
|
+
_setProp(name, value) {
|
|
13362
|
+
this.#propValues[name] = value;
|
|
13363
|
+
if (this.#app) {
|
|
13364
|
+
this.#app.bindings?.set(name, value);
|
|
13365
|
+
}
|
|
13366
|
+
else {
|
|
13367
|
+
this.#scheduleMountIfNeeded();
|
|
13368
|
+
}
|
|
13369
|
+
}
|
|
13370
|
+
/**
|
|
13371
|
+
* Called by the property getters generated by defineComponent().
|
|
13372
|
+
*/
|
|
13373
|
+
_getProp(name) {
|
|
13374
|
+
return this.#propValues[name];
|
|
13375
|
+
}
|
|
13376
|
+
// --- Static fields set by defineComponent() ---
|
|
13377
|
+
/**
|
|
13378
|
+
* CSS selector for the component's <template> element (e.g. '#my-card').
|
|
13379
|
+
*/
|
|
13380
|
+
static _template;
|
|
13381
|
+
/**
|
|
13382
|
+
* List of declared prop names. Used to decide whether to defer mounting.
|
|
13383
|
+
*/
|
|
13384
|
+
static _props = [];
|
|
13385
|
+
/**
|
|
13386
|
+
* Factory that builds VApplicationOptions from the current prop values.
|
|
13387
|
+
* Implemented by defineComponent() as a closure that captures the user's options.
|
|
13388
|
+
*/
|
|
13389
|
+
static _buildOptions;
|
|
13390
|
+
// --- Private helpers ---
|
|
13391
|
+
/**
|
|
13392
|
+
* Schedules a mount microtask if the DOM root is ready and no mount is pending.
|
|
13393
|
+
* Called from _setProp() so the mount happens after the prop value is stored.
|
|
13394
|
+
*/
|
|
13395
|
+
#scheduleMountIfNeeded() {
|
|
13396
|
+
if (this.#mountScheduled || this.#app || !this.#mountRoot) {
|
|
13397
|
+
return;
|
|
13398
|
+
}
|
|
13399
|
+
this.#mountScheduled = true;
|
|
13400
|
+
queueMicrotask(() => {
|
|
13401
|
+
this.#mountScheduled = false;
|
|
13402
|
+
this.#doMount();
|
|
13403
|
+
});
|
|
13404
|
+
}
|
|
13405
|
+
#doMount() {
|
|
13406
|
+
if (this.#app || !this.#mountRoot) {
|
|
13407
|
+
return;
|
|
13408
|
+
}
|
|
13409
|
+
const ctor = this.constructor;
|
|
13410
|
+
const options = ctor._buildOptions(this.#propValues);
|
|
13411
|
+
this.#app = VDOM.createApp(options);
|
|
13412
|
+
this.#app.mount(this.#mountRoot);
|
|
13413
|
+
}
|
|
13414
|
+
#findRootElement(fragment) {
|
|
13415
|
+
for (const node of Array.from(fragment.childNodes)) {
|
|
13416
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
13417
|
+
return node;
|
|
13418
|
+
}
|
|
13419
|
+
}
|
|
13420
|
+
throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
|
|
13421
|
+
}
|
|
13422
|
+
}
|
|
13423
|
+
|
|
13424
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
13425
|
+
/**
|
|
13426
|
+
* Defines and registers a custom element backed by ichigo.js reactivity.
|
|
13427
|
+
*
|
|
13428
|
+
* Usage:
|
|
13429
|
+
* ```html
|
|
13430
|
+
* <template id="my-list">
|
|
13431
|
+
* <div>
|
|
13432
|
+
* <ul v-if="items.length > 0">
|
|
13433
|
+
* <li v-for="item of items">{{item.name}}</li>
|
|
13434
|
+
* </ul>
|
|
13435
|
+
* <slot></slot>
|
|
13436
|
+
* </div>
|
|
13437
|
+
* </template>
|
|
13438
|
+
* ```
|
|
13439
|
+
* ```typescript
|
|
13440
|
+
* defineComponent('my-list', {
|
|
13441
|
+
* template: '#my-list',
|
|
13442
|
+
* props: ['items'],
|
|
13443
|
+
* data() {
|
|
13444
|
+
* return { items: this.items ?? [] };
|
|
13445
|
+
* },
|
|
13446
|
+
* });
|
|
13447
|
+
* ```
|
|
13448
|
+
* ```html
|
|
13449
|
+
* <my-list :items="searchResults">
|
|
13450
|
+
* <span slot="empty">No results.</span>
|
|
13451
|
+
* </my-list>
|
|
13452
|
+
* ```
|
|
13453
|
+
*
|
|
13454
|
+
* @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
|
|
13455
|
+
* @param options Component options including template selector and optional props.
|
|
13456
|
+
*/
|
|
13457
|
+
function defineComponent(tagName, options) {
|
|
13458
|
+
const { props = [], template, data, computed, methods, watch, logLevel } = options;
|
|
13459
|
+
// Build a subclass of IchigoElement specific to this component
|
|
13460
|
+
class ComponentElement extends IchigoElement {
|
|
13461
|
+
static _template = template;
|
|
13462
|
+
static _props = props;
|
|
13463
|
+
static _buildOptions(propValues) {
|
|
13464
|
+
return {
|
|
13465
|
+
data() {
|
|
13466
|
+
// 'this' is the $ctx object provided by VApplication ({ $markRaw }).
|
|
13467
|
+
// We extend it with prop values so the user's data() can reference them
|
|
13468
|
+
// via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
|
|
13469
|
+
const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
|
|
13470
|
+
const userData = data
|
|
13471
|
+
? data.call(ctx)
|
|
13472
|
+
: {};
|
|
13473
|
+
// Props are always included in data so they are reactive from the start.
|
|
13474
|
+
// User-returned values take precedence (allow transforming/defaulting props).
|
|
13475
|
+
return { ...propValues, ...userData };
|
|
13476
|
+
},
|
|
13477
|
+
computed,
|
|
13478
|
+
methods,
|
|
13479
|
+
watch,
|
|
13480
|
+
logLevel,
|
|
13481
|
+
};
|
|
13482
|
+
}
|
|
13483
|
+
}
|
|
13484
|
+
// Generate a property getter/setter for each declared prop.
|
|
13485
|
+
// This enables the parent VApplication to push updates via `element.propName = value`.
|
|
13486
|
+
for (const prop of props) {
|
|
13487
|
+
Object.defineProperty(ComponentElement.prototype, prop, {
|
|
13488
|
+
get() {
|
|
13489
|
+
return this._getProp(prop);
|
|
13490
|
+
},
|
|
13491
|
+
set(value) {
|
|
13492
|
+
this._setProp(prop, value);
|
|
13493
|
+
},
|
|
13494
|
+
configurable: true,
|
|
13495
|
+
enumerable: true,
|
|
13496
|
+
});
|
|
13497
|
+
}
|
|
13498
|
+
customElements.define(tagName, ComponentElement);
|
|
13499
|
+
}
|
|
13500
|
+
|
|
13052
13501
|
exports.ExpressionUtils = ExpressionUtils;
|
|
13502
|
+
exports.IchigoElement = IchigoElement;
|
|
13053
13503
|
exports.ReactiveProxy = ReactiveProxy;
|
|
13054
13504
|
exports.VComponent = VComponent;
|
|
13055
13505
|
exports.VComponentRegistry = VComponentRegistry;
|
|
13056
13506
|
exports.VDOM = VDOM;
|
|
13507
|
+
exports.defineComponent = defineComponent;
|
|
13057
13508
|
|
|
13058
13509
|
}));
|
|
13059
13510
|
//# sourceMappingURL=ichigo.cjs.map
|