@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.
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
2
2
  /**
3
3
  * Represents a reusable component definition.
4
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
4
5
  */
5
6
  class VComponent {
6
7
  /**
@@ -38,6 +39,7 @@ class VComponent {
38
39
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
39
40
  /**
40
41
  * A registry for managing component definitions.
42
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
41
43
  */
42
44
  class VComponentRegistry {
43
45
  /**
@@ -7468,6 +7470,14 @@ class VBindDirective {
7468
7470
  else if (attributeName === 'style') {
7469
7471
  this.#updateStyle(element, value);
7470
7472
  }
7473
+ else if (this.#isCustomElementProperty(element, attributeName, value)) {
7474
+ // Custom elements: set objects/arrays (and declared props) as properties
7475
+ // so complex values are not serialized to strings via setAttribute.
7476
+ // HTML attributes are lowercased by the browser, so resolve to the
7477
+ // actual camelCase property name declared on the custom element.
7478
+ const propName = this.#resolveCustomElementPropertyName(element, attributeName);
7479
+ element[propName] = value;
7480
+ }
7471
7481
  else if (this.#isDOMProperty(attributeName)) {
7472
7482
  this.#updateProperty(element, attributeName, value);
7473
7483
  }
@@ -7551,6 +7561,46 @@ class VBindDirective {
7551
7561
  #camelToKebab(str) {
7552
7562
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
7553
7563
  }
7564
+ /**
7565
+ * Returns true when the target is a custom element and the binding should be
7566
+ * delivered as a property rather than an HTML attribute.
7567
+ *
7568
+ * Two conditions trigger property delivery:
7569
+ * 1. The value is an object or array — serialising these to a string attribute
7570
+ * would lose type information.
7571
+ * 2. The element exposes a matching property accessor (e.g. a prop declared via
7572
+ * defineComponent), even when the value is a primitive.
7573
+ */
7574
+ #isCustomElementProperty(element, name, value) {
7575
+ if (!element.tagName.includes('-')) {
7576
+ return false;
7577
+ }
7578
+ const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
7579
+ return isObjectOrArray || name in element;
7580
+ }
7581
+ /**
7582
+ * Resolves the actual property name on a custom element for a given
7583
+ * (lowercased) HTML attribute name. HTML attributes are always lowercase,
7584
+ * but custom element props are typically camelCase. This method checks the
7585
+ * element's declared _props (set by defineComponent) for a case-insensitive
7586
+ * match, falling back to scanning the prototype's own property descriptors.
7587
+ */
7588
+ #resolveCustomElementPropertyName(element, name) {
7589
+ // Fast path: exact match already exists
7590
+ if (name in element) {
7591
+ return name;
7592
+ }
7593
+ // Check declared _props from defineComponent / IchigoElement
7594
+ const props = element.constructor._props;
7595
+ if (Array.isArray(props)) {
7596
+ const lowerName = name.toLowerCase();
7597
+ const match = props.find(p => p.toLowerCase() === lowerName);
7598
+ if (match) {
7599
+ return match;
7600
+ }
7601
+ }
7602
+ return name;
7603
+ }
7554
7604
  /**
7555
7605
  * Checks if the attribute should be set as a DOM property.
7556
7606
  */
@@ -7904,22 +7954,42 @@ class ReactiveProxy {
7904
7954
  const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7905
7955
  return ReactiveProxy.create(value, onChange, nestedPath);
7906
7956
  }
7907
- // For arrays and Maps, intercept mutation methods
7957
+ // If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
7908
7958
  if (typeof value === 'function') {
7909
- let mutationMethods = [];
7959
+ // For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
7910
7960
  if (Array.isArray(obj)) {
7911
- mutationMethods.push('push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse');
7961
+ const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
7962
+ if (!arrayMutationMethods.includes(key)) {
7963
+ return value;
7964
+ }
7965
+ return function (...args) {
7966
+ const result = value.apply(this === receiver ? obj : this, args);
7967
+ onChange(path || undefined);
7968
+ return result;
7969
+ };
7912
7970
  }
7913
- else if (obj.constructor.name === 'Map') {
7914
- mutationMethods.push('set', 'delete', 'clear');
7971
+ // For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
7972
+ if (obj.constructor.name === 'Map') {
7973
+ const mapMutationMethods = ['set', 'delete', 'clear'];
7974
+ return function (...args) {
7975
+ const result = value.apply(this === receiver ? obj : this, args);
7976
+ if (mapMutationMethods.includes(key)) {
7977
+ onChange(path || undefined);
7978
+ }
7979
+ return result;
7980
+ };
7981
+ }
7982
+ // For Set, we only want to wrap mutation methods, not read methods like 'has'
7983
+ if (obj.constructor.name === 'Set') {
7984
+ const setMutationMethods = ['add', 'delete', 'clear'];
7985
+ return function (...args) {
7986
+ const result = value.apply(this === receiver ? obj : this, args);
7987
+ if (setMutationMethods.includes(key)) {
7988
+ onChange(path || undefined);
7989
+ }
7990
+ return result;
7991
+ };
7915
7992
  }
7916
- return function (...args) {
7917
- const result = value.apply(this === receiver ? obj : this, args);
7918
- if (mutationMethods.includes(key)) {
7919
- onChange(path || undefined);
7920
- }
7921
- return result;
7922
- };
7923
7993
  }
7924
7994
  return value;
7925
7995
  },
@@ -8911,6 +8981,18 @@ class VNode {
8911
8981
  });
8912
8982
  }
8913
8983
  }
8984
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
8985
+ // If the node is a DocumentFragment, create child VNodes for its children
8986
+ // DocumentFragment is not an Element so it has no directives itself.
8987
+ this.#childVNodes = [];
8988
+ for (const childNode of Array.from(this.#node.childNodes)) {
8989
+ new VNode({
8990
+ node: childNode,
8991
+ vApplication: this.#vApplication,
8992
+ parentVNode: this
8993
+ });
8994
+ }
8995
+ }
8914
8996
  // Register this node as a dependent of the parent node, if any
8915
8997
  this.#closers = this.#parentVNode?.addDependent(this);
8916
8998
  }
@@ -9165,6 +9247,16 @@ class VNode {
9165
9247
  });
9166
9248
  }
9167
9249
  }
9250
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9251
+ // For document fragments (e.g. from <template v-if>), propagate updates
9252
+ // to dependent virtual nodes so that child bindings stay reactive.
9253
+ this.#dependents?.forEach(dependentNode => {
9254
+ const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
9255
+ if (changed) {
9256
+ dependentNode.update();
9257
+ }
9258
+ });
9259
+ }
9168
9260
  }
9169
9261
  /**
9170
9262
  * Forces an update of the virtual node and its children, regardless of changed identifiers.
@@ -9214,6 +9306,12 @@ class VNode {
9214
9306
  });
9215
9307
  }
9216
9308
  }
9309
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9310
+ // For document fragments, recursively force update child VNodes
9311
+ this.#childVNodes?.forEach(childVNode => {
9312
+ childVNode.forceUpdate();
9313
+ });
9314
+ }
9217
9315
  }
9218
9316
  /**
9219
9317
  * Adds a child virtual node to this virtual node.
@@ -9591,9 +9689,11 @@ class VConditionalDirective {
9591
9689
  }
9592
9690
  // Clone the original node and create a new VNode for it
9593
9691
  const clone = this.#cloneNode();
9594
- // Insert the cloned node after the anchor node, or as a child of the parent if no anchor
9595
- this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
9596
- // Create a new VNode for the cloned element
9692
+ // Create a new VNode for the cloned element BEFORE inserting into DOM.
9693
+ // This prevents custom elements (Web Components) from having their
9694
+ // connectedCallback fire before VNode processes their children, which
9695
+ // would cause the parent VApplication to incorrectly adopt the custom
9696
+ // element's internal template content as its own VNode tree.
9597
9697
  // Pass the current bindings to ensure loop variables from v-for are available
9598
9698
  const vNode = new VNode({
9599
9699
  node: clone,
@@ -9601,6 +9701,29 @@ class VConditionalDirective {
9601
9701
  parentVNode: this.#vNode,
9602
9702
  bindings: this.#vNode.bindings
9603
9703
  });
9704
+ // Insert after the anchor node AFTER VNode creation
9705
+ const anchorParent = this.#vNode.anchorNode?.parentNode;
9706
+ const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9707
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
9708
+ const startMarker = document.createComment('#vif-fragment-start');
9709
+ const endMarker = document.createComment('#vif-fragment-end');
9710
+ if (nextSibling) {
9711
+ anchorParent.insertBefore(startMarker, nextSibling);
9712
+ }
9713
+ else {
9714
+ anchorParent.appendChild(startMarker);
9715
+ }
9716
+ anchorParent.insertBefore(endMarker, startMarker.nextSibling);
9717
+ anchorParent.insertBefore(clone, endMarker);
9718
+ // Store markers for later removal
9719
+ vNode.userData.set('vif_fragment_start', startMarker);
9720
+ vNode.userData.set('vif_fragment_end', endMarker);
9721
+ this.#renderedVNode = vNode;
9722
+ this.#renderedVNode.forceUpdate();
9723
+ return;
9724
+ }
9725
+ const nodeToInsert = vNode.anchorNode || clone;
9726
+ anchorParent?.insertBefore(nodeToInsert, nextSibling);
9604
9727
  this.#renderedVNode = vNode;
9605
9728
  this.#renderedVNode.forceUpdate();
9606
9729
  }
@@ -9615,17 +9738,40 @@ class VConditionalDirective {
9615
9738
  }
9616
9739
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9617
9740
  this.#renderedVNode.destroy();
9618
- // Then remove from DOM
9619
- this.#renderedVNode.node.parentNode?.removeChild(this.#renderedVNode.node);
9741
+ // Then remove from DOM. Handle fragment markers if present
9742
+ const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
9743
+ const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
9744
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
9745
+ const parentNode = startMarker.parentNode;
9746
+ let node = startMarker;
9747
+ while (node) {
9748
+ const next = node.nextSibling;
9749
+ parentNode.removeChild(node);
9750
+ if (node === endMarker)
9751
+ break;
9752
+ node = next;
9753
+ }
9754
+ this.#renderedVNode = undefined;
9755
+ return;
9756
+ }
9757
+ const parent = this.#renderedVNode.node.parentNode;
9758
+ if (parent) {
9759
+ parent.removeChild(this.#renderedVNode.node);
9760
+ }
9620
9761
  this.#renderedVNode = undefined;
9621
9762
  }
9622
9763
  /**
9623
9764
  * Clones the original node of the directive's virtual node.
9624
- * This is used to create a new instance of the node for rendering.
9625
- * @returns The cloned HTMLElement.
9765
+ * When the source element is a <template>, returns a DocumentFragment
9766
+ * cloned from the template's content.
9767
+ * @returns The cloned Node (HTMLElement or DocumentFragment).
9626
9768
  */
9627
9769
  #cloneNode() {
9628
9770
  const element = this.#vNode.node;
9771
+ if (element instanceof HTMLTemplateElement) {
9772
+ // Return a DocumentFragment cloned from the template content
9773
+ return element.content.cloneNode(true);
9774
+ }
9629
9775
  return element.cloneNode(true);
9630
9776
  }
9631
9777
  /**
@@ -9943,10 +10089,28 @@ class VForDirective {
9943
10089
  vNode.destroy();
9944
10090
  }
9945
10091
  }
9946
- // Then remove from DOM
10092
+ // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
9947
10093
  for (const vNode of nodesToRemove) {
9948
- if (vNode.node.parentNode) {
9949
- vNode.node.parentNode.removeChild(vNode.node);
10094
+ // If this VNode stored fragment markers, remove the range between them
10095
+ const startMarker = vNode.userData.get?.('vfor_fragment_start');
10096
+ const endMarker = vNode.userData.get?.('vfor_fragment_end');
10097
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
10098
+ const parentNode = startMarker.parentNode;
10099
+ let node = startMarker;
10100
+ // Remove nodes from startMarker up to and including endMarker
10101
+ while (node) {
10102
+ const next = node.nextSibling;
10103
+ parentNode.removeChild(node);
10104
+ if (node === endMarker)
10105
+ break;
10106
+ node = next;
10107
+ }
10108
+ continue;
10109
+ }
10110
+ // Fallback: remove the node itself if it's attached
10111
+ const parentOfNode = vNode.node.parentNode;
10112
+ if (parentOfNode) {
10113
+ parentOfNode.removeChild(vNode.node);
9950
10114
  }
9951
10115
  }
9952
10116
  // Add or reorder items
@@ -9980,6 +10144,28 @@ class VForDirective {
9980
10144
  bindings,
9981
10145
  dependentIdentifiers: depIds,
9982
10146
  });
10147
+ // If clone is a DocumentFragment, insert it between start/end comment markers
10148
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10149
+ const startMarker = document.createComment('#vfor-fragment-start');
10150
+ const endMarker = document.createComment('#vfor-fragment-end');
10151
+ // Insert start and end markers and the fragment's children between them
10152
+ if (prevNode.nextSibling) {
10153
+ parent.insertBefore(startMarker, prevNode.nextSibling);
10154
+ }
10155
+ else {
10156
+ parent.appendChild(startMarker);
10157
+ }
10158
+ parent.insertBefore(endMarker, startMarker.nextSibling);
10159
+ parent.insertBefore(clone, endMarker);
10160
+ // Store markers on the VNode for later removal/movement
10161
+ vNode.userData.set('vfor_fragment_start', startMarker);
10162
+ vNode.userData.set('vfor_fragment_end', endMarker);
10163
+ newRenderedItems.set(key, vNode);
10164
+ vNode.forceUpdate();
10165
+ // Use endMarker as prevNode for subsequent insertions
10166
+ prevNode = endMarker;
10167
+ continue;
10168
+ }
9983
10169
  // Determine what to insert: anchor node (if exists) or the clone itself
9984
10170
  const nodeToInsert = vNode.anchorNode || clone;
9985
10171
  // Insert after previous node
@@ -9997,8 +10183,9 @@ class VForDirective {
9997
10183
  newRenderedItems.set(key, vNode);
9998
10184
  // Update bindings
9999
10185
  this.#updateItemBindings(vNode, context);
10000
- // Determine the actual node in DOM: anchor node (if exists) or vNode.node
10001
- const actualNode = vNode.anchorNode || vNode.node;
10186
+ // Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
10187
+ const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
10188
+ const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
10002
10189
  // Move to correct position if needed
10003
10190
  if (prevNode.nextSibling !== actualNode) {
10004
10191
  if (prevNode.nextSibling) {
@@ -10009,8 +10196,9 @@ class VForDirective {
10009
10196
  }
10010
10197
  }
10011
10198
  }
10012
- // Use anchor node as prevNode if it exists, otherwise use vNode.node
10013
- prevNode = vNode.anchorNode || vNode.node;
10199
+ // Use fragment end marker > anchor node > vNode.node as prevNode
10200
+ const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
10201
+ prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
10014
10202
  }
10015
10203
  // Update rendered items map
10016
10204
  this.#renderedItems = newRenderedItems;
@@ -10189,12 +10377,17 @@ class VForDirective {
10189
10377
  }
10190
10378
  /**
10191
10379
  * Clones the original node of the directive's virtual node.
10192
- * This is used to create a new instance of the node for rendering.
10193
- * @returns The cloned HTMLElement.
10380
+ * When the source element is a <template>, its children (stored in .content)
10381
+ * are cloned into a DocumentFragment so that multiple root nodes can be
10382
+ * managed without adding an extra wrapper element.
10383
+ * @returns The cloned Node (Element or DocumentFragment).
10194
10384
  */
10195
10385
  #cloneNode() {
10196
- // Clone the original element
10197
10386
  const element = this.#vNode.node;
10387
+ if (element instanceof HTMLTemplateElement) {
10388
+ // Return a cloned DocumentFragment containing the template content
10389
+ return element.content.cloneNode(true);
10390
+ }
10198
10391
  return element.cloneNode(true);
10199
10392
  }
10200
10393
  /**
@@ -13000,6 +13193,7 @@ class VDOM {
13000
13193
  /**
13001
13194
  * Gets the component registry.
13002
13195
  * @return {VComponentRegistry} The component registry.
13196
+ * @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
13003
13197
  */
13004
13198
  static get componentRegistry() {
13005
13199
  return this.#componentRegistry;
@@ -13043,5 +13237,260 @@ class VDOM {
13043
13237
  }
13044
13238
  }
13045
13239
 
13046
- export { ExpressionUtils, ReactiveProxy, VComponent, VComponentRegistry, VDOM };
13240
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13241
+ /**
13242
+ * Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
13243
+ *
13244
+ * Mount timing:
13245
+ * - If the component declares no props, the VApplication is mounted synchronously
13246
+ * at the end of connectedCallback (the template is known, no props to wait for).
13247
+ * - If the component declares props, the VApplication is mounted the first time
13248
+ * _setProp() is called after connectedCallback has prepared the DOM. This
13249
+ * guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
13250
+ * already delivered at least one prop value before data() is evaluated.
13251
+ *
13252
+ * Subclasses must set the static fields _template, _props and _buildOptions before
13253
+ * calling customElements.define(). defineComponent() handles this automatically.
13254
+ */
13255
+ class IchigoElement extends HTMLElement {
13256
+ /**
13257
+ * The mounted VApplication instance, present only while connected to the DOM.
13258
+ */
13259
+ #app;
13260
+ /**
13261
+ * Stores prop values received at any time (before or after mount).
13262
+ */
13263
+ #propValues = {};
13264
+ /**
13265
+ * The root element cloned from the template, ready for mounting.
13266
+ * Set by connectedCallback; cleared on disconnect.
13267
+ */
13268
+ #mountRoot;
13269
+ /**
13270
+ * Whether a mount microtask is already queued to avoid double-mounting.
13271
+ */
13272
+ #mountScheduled = false;
13273
+ connectedCallback() {
13274
+ // --- 0. Guard: skip if template not yet loaded ---
13275
+ // customElements.define() may run before loadComponent() completes (e.g. when
13276
+ // dynamic imports are inlined by the bundler). The static placeholder element
13277
+ // in the HTML will be removed and replaced by v-for once the template is ready,
13278
+ // so it is safe to do nothing here.
13279
+ const ctor = this.constructor;
13280
+ const templateEl = document.querySelector(ctor._template);
13281
+ if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
13282
+ return;
13283
+ }
13284
+ // --- 1. Capture slot content before clearing children ---
13285
+ const defaultSlotNodes = [];
13286
+ const namedSlotNodes = new Map();
13287
+ for (const child of Array.from(this.childNodes)) {
13288
+ if (child.nodeType === Node.ELEMENT_NODE) {
13289
+ const el = child;
13290
+ const slotName = el.getAttribute('slot');
13291
+ if (slotName) {
13292
+ if (!namedSlotNodes.has(slotName)) {
13293
+ namedSlotNodes.set(slotName, []);
13294
+ }
13295
+ namedSlotNodes.get(slotName).push(el);
13296
+ // Remove slot attribute so ichigo.js doesn't try to bind it
13297
+ el.removeAttribute('slot');
13298
+ }
13299
+ else {
13300
+ defaultSlotNodes.push(el);
13301
+ }
13302
+ }
13303
+ else if (child.nodeType === Node.TEXT_NODE) {
13304
+ if ((child.textContent ?? '').trim()) {
13305
+ defaultSlotNodes.push(child);
13306
+ }
13307
+ }
13308
+ }
13309
+ // Clear host element so we can append the cloned template
13310
+ while (this.firstChild) {
13311
+ this.removeChild(this.firstChild);
13312
+ }
13313
+ // --- 2. Clone the component template ---
13314
+ const fragment = templateEl.content.cloneNode(true);
13315
+ const root = this.#findRootElement(fragment);
13316
+ // --- 3. Distribute named slot content ---
13317
+ for (const [name, nodes] of namedSlotNodes) {
13318
+ const slot = root.querySelector(`slot[name="${name}"]`);
13319
+ if (slot) {
13320
+ slot.replaceWith(...nodes);
13321
+ }
13322
+ }
13323
+ // --- 4. Distribute default slot content ---
13324
+ const defaultSlot = root.querySelector('slot:not([name])');
13325
+ if (defaultSlot && defaultSlotNodes.length > 0) {
13326
+ defaultSlot.replaceWith(...defaultSlotNodes);
13327
+ }
13328
+ // Attach the populated template to the host element
13329
+ this.appendChild(root);
13330
+ this.#mountRoot = root;
13331
+ // If props were set before connectedCallback, ensure mount is scheduled
13332
+ this.#scheduleMountIfNeeded();
13333
+ // --- 5. Mount strategy ---
13334
+ // If this component has no declared props, mount immediately.
13335
+ // If it has props, mount will be triggered from _setProp() once the parent
13336
+ // delivers the first prop value (via VBindDirective / forceUpdate).
13337
+ if (ctor._props.length === 0) {
13338
+ this.#doMount();
13339
+ }
13340
+ // else: wait for _setProp() to trigger #scheduleMountIfNeeded()
13341
+ }
13342
+ disconnectedCallback() {
13343
+ this.#mountRoot = undefined;
13344
+ this.#mountScheduled = false;
13345
+ if (this.#app) {
13346
+ this.#app.unmount();
13347
+ this.#app = undefined;
13348
+ }
13349
+ }
13350
+ /**
13351
+ * Called by the property setters generated by defineComponent().
13352
+ * Before mount: stores the value and schedules a mount microtask.
13353
+ * After mount: pushes the value directly into the reactive bindings.
13354
+ */
13355
+ _setProp(name, value) {
13356
+ this.#propValues[name] = value;
13357
+ if (this.#app) {
13358
+ this.#app.bindings?.set(name, value);
13359
+ }
13360
+ else {
13361
+ this.#scheduleMountIfNeeded();
13362
+ }
13363
+ }
13364
+ /**
13365
+ * Called by the property getters generated by defineComponent().
13366
+ */
13367
+ _getProp(name) {
13368
+ return this.#propValues[name];
13369
+ }
13370
+ // --- Static fields set by defineComponent() ---
13371
+ /**
13372
+ * CSS selector for the component's <template> element (e.g. '#my-card').
13373
+ */
13374
+ static _template;
13375
+ /**
13376
+ * List of declared prop names. Used to decide whether to defer mounting.
13377
+ */
13378
+ static _props = [];
13379
+ /**
13380
+ * Factory that builds VApplicationOptions from the current prop values.
13381
+ * Implemented by defineComponent() as a closure that captures the user's options.
13382
+ */
13383
+ static _buildOptions;
13384
+ // --- Private helpers ---
13385
+ /**
13386
+ * Schedules a mount microtask if the DOM root is ready and no mount is pending.
13387
+ * Called from _setProp() so the mount happens after the prop value is stored.
13388
+ */
13389
+ #scheduleMountIfNeeded() {
13390
+ if (this.#mountScheduled || this.#app || !this.#mountRoot) {
13391
+ return;
13392
+ }
13393
+ this.#mountScheduled = true;
13394
+ queueMicrotask(() => {
13395
+ this.#mountScheduled = false;
13396
+ this.#doMount();
13397
+ });
13398
+ }
13399
+ #doMount() {
13400
+ if (this.#app || !this.#mountRoot) {
13401
+ return;
13402
+ }
13403
+ const ctor = this.constructor;
13404
+ const options = ctor._buildOptions(this.#propValues);
13405
+ this.#app = VDOM.createApp(options);
13406
+ this.#app.mount(this.#mountRoot);
13407
+ }
13408
+ #findRootElement(fragment) {
13409
+ for (const node of Array.from(fragment.childNodes)) {
13410
+ if (node.nodeType === Node.ELEMENT_NODE) {
13411
+ return node;
13412
+ }
13413
+ }
13414
+ throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
13415
+ }
13416
+ }
13417
+
13418
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13419
+ /**
13420
+ * Defines and registers a custom element backed by ichigo.js reactivity.
13421
+ *
13422
+ * Usage:
13423
+ * ```html
13424
+ * <template id="my-list">
13425
+ * <div>
13426
+ * <ul v-if="items.length > 0">
13427
+ * <li v-for="item of items">{{item.name}}</li>
13428
+ * </ul>
13429
+ * <slot></slot>
13430
+ * </div>
13431
+ * </template>
13432
+ * ```
13433
+ * ```typescript
13434
+ * defineComponent('my-list', {
13435
+ * template: '#my-list',
13436
+ * props: ['items'],
13437
+ * data() {
13438
+ * return { items: this.items ?? [] };
13439
+ * },
13440
+ * });
13441
+ * ```
13442
+ * ```html
13443
+ * <my-list :items="searchResults">
13444
+ * <span slot="empty">No results.</span>
13445
+ * </my-list>
13446
+ * ```
13447
+ *
13448
+ * @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
13449
+ * @param options Component options including template selector and optional props.
13450
+ */
13451
+ function defineComponent(tagName, options) {
13452
+ const { props = [], template, data, computed, methods, watch, logLevel } = options;
13453
+ // Build a subclass of IchigoElement specific to this component
13454
+ class ComponentElement extends IchigoElement {
13455
+ static _template = template;
13456
+ static _props = props;
13457
+ static _buildOptions(propValues) {
13458
+ return {
13459
+ data() {
13460
+ // 'this' is the $ctx object provided by VApplication ({ $markRaw }).
13461
+ // We extend it with prop values so the user's data() can reference them
13462
+ // via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
13463
+ const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
13464
+ const userData = data
13465
+ ? data.call(ctx)
13466
+ : {};
13467
+ // Props are always included in data so they are reactive from the start.
13468
+ // User-returned values take precedence (allow transforming/defaulting props).
13469
+ return { ...propValues, ...userData };
13470
+ },
13471
+ computed,
13472
+ methods,
13473
+ watch,
13474
+ logLevel,
13475
+ };
13476
+ }
13477
+ }
13478
+ // Generate a property getter/setter for each declared prop.
13479
+ // This enables the parent VApplication to push updates via `element.propName = value`.
13480
+ for (const prop of props) {
13481
+ Object.defineProperty(ComponentElement.prototype, prop, {
13482
+ get() {
13483
+ return this._getProp(prop);
13484
+ },
13485
+ set(value) {
13486
+ this._setProp(prop, value);
13487
+ },
13488
+ configurable: true,
13489
+ enumerable: true,
13490
+ });
13491
+ }
13492
+ customElements.define(tagName, ComponentElement);
13493
+ }
13494
+
13495
+ export { ExpressionUtils, IchigoElement, ReactiveProxy, VComponent, VComponentRegistry, VDOM, defineComponent };
13047
13496
  //# sourceMappingURL=ichigo.esm.js.map