@mintjamsinc/ichigojs 0.1.79 → 0.1.80

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.
@@ -9218,8 +9218,9 @@ class VSlotOutletDirective {
9218
9218
  this.#scopeBindings.setLocal(this.#entry.scopeVar, this.#buildSlotProps());
9219
9219
  }
9220
9220
  // Clone the scoped template's content and create its VNode tree BEFORE
9221
- // inserting into the DOM, so components inside the slot content are
9222
- // compiled before their connectedCallback can expand them.
9221
+ // inserting into the DOM, so its directives run against a subtree
9222
+ // nothing else can observe yet. Components inside are expanded
9223
+ // afterwards, once the content is in the document.
9223
9224
  // (Created through the parent application to keep the VNode import
9224
9225
  // type-only; a direct `new VNode` here would close a module cycle
9225
9226
  // VNode -> VDirectiveManager -> VSlotOutletDirective -> VNode.)
@@ -9238,6 +9239,8 @@ class VSlotOutletDirective {
9238
9239
  slot.replaceWith(fragment);
9239
9240
  // Initial rendering
9240
9241
  this.#contentVNode.forceUpdate();
9242
+ // Components in the slot content expand once it is in the document.
9243
+ this.#contentVNode.expandComponents();
9241
9244
  }
9242
9245
  /**
9243
9246
  * Re-evaluates the slot props (component-side change) and pushes them into
@@ -9854,6 +9857,31 @@ class VNode {
9854
9857
  * remove the entire fragment atomically.
9855
9858
  */
9856
9859
  #fragmentRange;
9860
+ /**
9861
+ * Custom element tags already reported by
9862
+ * {@link VNode.#warnIfUnknownCustomElement}.
9863
+ */
9864
+ static #warnedUnknownTags = new Set();
9865
+ /**
9866
+ * Warns about a hyphenated tag that no one has registered — a forgotten
9867
+ * import, a missing defineComponent() call, or a typo in the tag name.
9868
+ * Such an element is compiled as an ordinary element and renders its own
9869
+ * children, which looks enough like a working page to cost an afternoon.
9870
+ * Reported once per tag; third-party custom elements are recognised
9871
+ * through the registry the browser keeps.
9872
+ */
9873
+ static #warnIfUnknownCustomElement(nodeName) {
9874
+ const tag = nodeName.toLowerCase();
9875
+ if (!tag.includes('-') || VNode.#warnedUnknownTags.has(tag)) {
9876
+ return;
9877
+ }
9878
+ if (typeof customElements !== 'undefined' && customElements.get(tag)) {
9879
+ return;
9880
+ }
9881
+ VNode.#warnedUnknownTags.add(tag);
9882
+ console.warn(`[ichigo] <${tag}>: unknown custom element — nothing has registered this tag.`
9883
+ + ' Did you forget to import its module or call defineComponent(), or is the tag misspelled?');
9884
+ }
9857
9885
  /**
9858
9886
  * Creates an instance of the virtual node.
9859
9887
  * @param args The initialization arguments for the virtual node.
@@ -9885,6 +9913,9 @@ class VNode {
9885
9913
  if (isComponent) {
9886
9914
  element._ichigoHost = { vNode: this };
9887
9915
  }
9916
+ else {
9917
+ VNode.#warnIfUnknownCustomElement(this.#nodeName);
9918
+ }
9888
9919
  // Initialize child virtual nodes
9889
9920
  this.#childVNodes = [];
9890
9921
  // Initialize directive manager
@@ -9918,16 +9949,6 @@ class VNode {
9918
9949
  });
9919
9950
  }
9920
9951
  }
9921
- // A component element this application compiled but that has not
9922
- // expanded yet — it deferred to us because an ancestor component
9923
- // was attaching its template, or its <template> was not loaded when
9924
- // it connected — is expanded now: the host context is installed,
9925
- // the directives above delivered the prop values, and any authored
9926
- // slot content was compiled in this scope. A template source
9927
- // (v-if / v-for) is left alone; its clones expand on insertion.
9928
- if (isComponent && !this.#templatized && !isExpandedComponent) {
9929
- element._ichigoTryExpand?.();
9930
- }
9931
9952
  // After creating child nodes, call onMounted for directives that do not templatize
9932
9953
  if (!this.#templatized) {
9933
9954
  // animation frame to ensure DOM is updated
@@ -9953,6 +9974,37 @@ class VNode {
9953
9974
  // Register this node as a dependent of the parent node, if any
9954
9975
  this.#closers = this.#parentVNode?.addDependent(this);
9955
9976
  }
9977
+ /**
9978
+ * Expands the component elements this application compiled in this subtree
9979
+ * but has not expanded yet, parents before their children.
9980
+ *
9981
+ * This is the ONLY thing that expands a component. It is deliberately a
9982
+ * separate step from compilation rather than a call at the end of the
9983
+ * constructor: a v-if / v-for clone is compiled while it is still detached
9984
+ * (so that its directives run before anything can observe a half-built
9985
+ * subtree), and expanding a detached element would mount a component
9986
+ * application outside the document. The callers therefore compile, insert,
9987
+ * and then call this — see VApplication.mount, VConditionalDirective,
9988
+ * VForDirective and VSlotOutletDirective.
9989
+ *
9990
+ * Walking only {@link childVNodes} keeps the traversal inside this
9991
+ * application: the internals of an expanded component were never compiled
9992
+ * here, so they are not reachable and are left to their own application.
9993
+ * A templatized node is skipped whole — it is a template source, and its
9994
+ * clones expand when they are inserted.
9995
+ */
9996
+ expandComponents() {
9997
+ if (this.#templatized) {
9998
+ return;
9999
+ }
10000
+ if (this.#nodeType === Node.ELEMENT_NODE && IchigoElementRegistry.has(this.#nodeName)) {
10001
+ const element = this.#node;
10002
+ if (element._ichigoExpanded !== true) {
10003
+ element._ichigoExpand?.();
10004
+ }
10005
+ }
10006
+ this.#childVNodes?.forEach(child => child.expandComponents());
10007
+ }
9956
10008
  /**
9957
10009
  * The application instance associated with this virtual node.
9958
10010
  */
@@ -10753,11 +10805,10 @@ class VConditionalDirective {
10753
10805
  }
10754
10806
  // Clone the original node and create a new VNode for it
10755
10807
  const clone = this.#cloneNode();
10756
- // Create a new VNode for the cloned element BEFORE inserting into DOM.
10757
- // This prevents custom elements (Web Components) from having their
10758
- // connectedCallback fire before VNode processes their children, which
10759
- // would cause the parent VApplication to incorrectly adopt the custom
10760
- // element's internal template content as its own VNode tree.
10808
+ // Compile the clone BEFORE inserting it, so its directives run against a
10809
+ // subtree nothing else can observe yet (v-if inside creates anchors and
10810
+ // detaches template sources). Components inside are expanded afterwards,
10811
+ // once the clone is in the document — see expandComponents() below.
10761
10812
  // Pass the current bindings to ensure loop variables from v-for are available
10762
10813
  const vNode = new VNode({
10763
10814
  node: clone,
@@ -10772,12 +10823,16 @@ class VConditionalDirective {
10772
10823
  vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
10773
10824
  this.#renderedVNode = vNode;
10774
10825
  this.#renderedVNode.forceUpdate();
10826
+ this.#renderedVNode.expandComponents();
10775
10827
  return;
10776
10828
  }
10777
10829
  const nodeToInsert = vNode.anchorNode || clone;
10778
10830
  anchorParent?.insertBefore(nodeToInsert, nextSibling);
10779
10831
  this.#renderedVNode = vNode;
10780
10832
  this.#renderedVNode.forceUpdate();
10833
+ // Components expand once the clone is in the document and its bindings
10834
+ // have been applied, so they mount connected and with their props set.
10835
+ this.#renderedVNode.expandComponents();
10781
10836
  }
10782
10837
  /**
10783
10838
  * Removes the node from the DOM.
@@ -11234,6 +11289,7 @@ class VForDirective {
11234
11289
  vNode.fragmentRange = range;
11235
11290
  newRenderedItems.push({ key, vNode });
11236
11291
  vNode.forceUpdate();
11292
+ vNode.expandComponents();
11237
11293
  prevNode = range.lastNode;
11238
11294
  continue;
11239
11295
  }
@@ -11248,6 +11304,10 @@ class VForDirective {
11248
11304
  }
11249
11305
  newRenderedItems.push({ key, vNode });
11250
11306
  vNode.forceUpdate();
11307
+ // Components expand once the clone is in the document and its
11308
+ // bindings have been applied, so they mount connected and with
11309
+ // their props set.
11310
+ vNode.expandComponents();
11251
11311
  }
11252
11312
  else {
11253
11313
  // Reuse existing item
@@ -14817,6 +14877,10 @@ class VApplication {
14817
14877
  });
14818
14878
  // Initial rendering
14819
14879
  this.#vNode.update();
14880
+ // Expand the components in the tree. Last, so every component element
14881
+ // has been compiled and has had its prop values delivered before its
14882
+ // template is cloned in. See VNode.expandComponents.
14883
+ this.#vNode.expandComponents();
14820
14884
  // Remove v-cloak attributes after mounting
14821
14885
  // v-cloak is used to hide un-compiled template until the app is ready
14822
14886
  // Usage: Add CSS rule "[v-cloak] { display: none; }" to hide elements with v-cloak
@@ -15381,6 +15445,19 @@ class VDOM {
15381
15445
  /**
15382
15446
  * Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
15383
15447
  *
15448
+ * Expansion — cloning the template into this element — is driven by the
15449
+ * application that owns the element, not by connectedCallback: VNode compiles
15450
+ * the element first (installing the host context, delivering the prop values
15451
+ * and compiling the authored slot content in the owner's scope) and only then
15452
+ * calls {@link _ichigoExpand}. That leaves connectedCallback a single job,
15453
+ * re-attachment; see there.
15454
+ *
15455
+ * The component's <template> must be present in the document, and
15456
+ * defineComponent() must have run, before the owning application is mounted.
15457
+ * That is the application's responsibility. A missing template is reported as
15458
+ * a warning and the element is left empty — it is never silently retried,
15459
+ * because an app that renders anyway is an app whose bug cannot be found.
15460
+ *
15384
15461
  * Mount timing:
15385
15462
  * - If the component declares no props, the VApplication is mounted synchronously
15386
15463
  * at the end of connectedCallback (the template is known, no props to wait for).
@@ -15440,38 +15517,42 @@ class IchigoElement extends HTMLElement {
15440
15517
  get _ichigoScopedSlots() {
15441
15518
  return this.#slotCache?.scopedSlots;
15442
15519
  }
15520
+ /**
15521
+ * Handles RE-attachment, and nothing else.
15522
+ *
15523
+ * A first connection is not what expands this element: the owning
15524
+ * application does that from {@link _ichigoExpand} once it has compiled the
15525
+ * element. So a connection event on a not-yet-expanded element means the
15526
+ * owner has not reached it yet, and the right thing to do is nothing. That
15527
+ * covers every case the old guards enumerated one by one — a v-if / v-for
15528
+ * template source, an element inside an enclosing component's template
15529
+ * clone, a clone inserted before its directive expands it.
15530
+ *
15531
+ * An expanded element whose mount root has been torn down, on the other
15532
+ * hand, was disconnected and is now coming back: v-for reorders relocate
15533
+ * the host element, which disconnects and reconnects it. Its children are
15534
+ * the previous expansion, not authored slot content, so it re-expands from
15535
+ * the cached slot nodes. Testing the mount root rather than the expansion
15536
+ * flag alone keeps this from firing on an element that was expanded while
15537
+ * detached and is being inserted for the first time.
15538
+ */
15443
15539
  connectedCallback() {
15444
- // --- 0a. Guard: do not expand a template-source element ---
15445
- // An element still carrying a templatizing directive attribute (v-if
15446
- // family / v-for) is a template source: the owning application clones it
15447
- // and inserts the clones (whose directive attributes are stripped during
15448
- // clone compilation, so the clones expand normally). Expanding the source
15449
- // here would bake the expansion into every clone, and the clone's
15450
- // internals would then be compiled in the parent scope.
15451
- if (this.hasAttribute(StandardDirectiveName.V_IF) ||
15452
- this.hasAttribute(StandardDirectiveName.V_ELSE_IF) ||
15453
- this.hasAttribute(StandardDirectiveName.V_ELSE) ||
15454
- this.hasAttribute(StandardDirectiveName.V_FOR)) {
15455
- return;
15456
- }
15457
- // --- 0a2. Guard: wait to be compiled by the owning application ---
15458
- // This element came from an enclosing component's template clone and no
15459
- // application has compiled it yet: the enclosing component's appendChild
15460
- // connected it before that component's VApplication existed. Expanding
15461
- // now would capture the slot content and mount before the owning
15462
- // application installs the host context (_ichigoHost) and delivers the
15463
- // prop values — scoped slot outlets would find no parent scope and
15464
- // silently fall back to their default content. The owning application
15465
- // expands this element while it compiles it (VNode ->
15466
- // _ichigoTryExpand()), by which time both are in place.
15467
- //
15468
- // Only template-owned elements defer: authored slot content placed
15469
- // inside a component by an application is not part of the template, so
15470
- // it keeps expanding immediately (and is redistributed by the enclosing
15471
- // component on every re-expansion).
15472
- if (this._ichigoTemplateOwned === true && !this._ichigoHost) {
15473
- return;
15540
+ if (this._ichigoExpanded && !this.#mountRoot) {
15541
+ this._ichigoExpand();
15474
15542
  }
15543
+ }
15544
+ /**
15545
+ * Expands this element's template. Called by the application that owns the
15546
+ * element, right after it compiled it (VNode.expandComponents): the host
15547
+ * context is installed, the prop bindings have delivered their values and
15548
+ * the authored slot content has been compiled in the owner's scope, so the
15549
+ * capture below sees owner-compiled nodes and the component's own
15550
+ * application starts with its props already in hand.
15551
+ *
15552
+ * Also called by {@link connectedCallback} to rebuild the expansion after a
15553
+ * move.
15554
+ */
15555
+ _ichigoExpand() {
15475
15556
  // --- 0b. Reclaim prop values assigned before upgrade ---
15476
15557
  // A plain property set on the instance (e.g. by a binding delivered
15477
15558
  // while the element was detached or not yet upgraded) is an own data
@@ -15487,13 +15568,15 @@ class IchigoElement extends HTMLElement {
15487
15568
  this[prop] = value;
15488
15569
  }
15489
15570
  }
15490
- // --- 0c. Guard: skip if template not yet loaded ---
15491
- // customElements.define() may run before loadComponent() completes (e.g. when
15492
- // dynamic imports are inlined by the bundler). The static placeholder element
15493
- // in the HTML will be removed and replaced by v-for once the template is ready,
15494
- // so it is safe to do nothing here.
15571
+ // --- 0c. Resolve the template ---
15572
+ // Having the <template> in the document by now is the application's
15573
+ // contract (see the class doc). Report the breach and leave the element
15574
+ // empty: rendering something plausible here would hide the bug at the
15575
+ // one moment it is still cheap to find.
15495
15576
  const templateEl = document.querySelector(ctor._template);
15496
15577
  if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
15578
+ IchigoElement.#warnOncePerTag(this.tagName, `template '${ctor._template}' was not found in the document. `
15579
+ + 'Inject the <template> and call defineComponent() before mounting the application.');
15497
15580
  return;
15498
15581
  }
15499
15582
  // --- 1. Capture slot content (first connection only) ---
@@ -15545,18 +15628,6 @@ class IchigoElement extends HTMLElement {
15545
15628
  // --- 2. Clone the component template ---
15546
15629
  const fragment = templateEl.content.cloneNode(true);
15547
15630
  const root = this.#findRootElement(fragment);
15548
- // Mark the component elements the template brought in, so they let this
15549
- // component's application compile them before they expand (guard 0a2).
15550
- // Marked before slot distribution, so authored slot content — which is
15551
- // not part of the template — keeps expanding under the application that
15552
- // authored it. Elements inside a nested <template> are not marked: they
15553
- // are inert until a directive clones them, and the clone is compiled
15554
- // before it is inserted.
15555
- for (const element of Array.from(fragment.querySelectorAll('*'))) {
15556
- if (IchigoElementRegistry.has(element.tagName)) {
15557
- element._ichigoTemplateOwned = true;
15558
- }
15559
- }
15560
15631
  // --- 3. Distribute named slot content ---
15561
15632
  for (const [name, nodes] of namedNodes) {
15562
15633
  const slot = root.querySelector(`slot[name="${name}"]`);
@@ -15594,21 +15665,6 @@ class IchigoElement extends HTMLElement {
15594
15665
  this.#app = undefined;
15595
15666
  }
15596
15667
  }
15597
- /**
15598
- * Expands this element's template on behalf of the application that owns
15599
- * it. Called by VNode right after it compiled this element — the host
15600
- * context is installed and the prop bindings have delivered their values,
15601
- * so a component that deferred its expansion (guard 0a2, or a template
15602
- * that was not loaded when it connected) can now expand in the right
15603
- * order. A no-op when the element is already expanded or not connected
15604
- * (a template-source clone expands when it is inserted).
15605
- */
15606
- _ichigoTryExpand() {
15607
- if (this._ichigoExpanded || !this.isConnected) {
15608
- return;
15609
- }
15610
- this.connectedCallback();
15611
- }
15612
15668
  /**
15613
15669
  * Called by the property setters generated by defineComponent().
15614
15670
  * Before mount: stores the value and schedules a mount microtask.
@@ -15835,6 +15891,23 @@ class IchigoElement extends HTMLElement {
15835
15891
  #warn(message) {
15836
15892
  console.warn(`[ichigo] <${this.tagName.toLowerCase()}>: ${message}`);
15837
15893
  }
15894
+ /**
15895
+ * Tags already reported by {@link #warnOncePerTag}.
15896
+ */
15897
+ static #warnedTags = new Set();
15898
+ /**
15899
+ * Reports a per-component-type problem once. A missing template breaks
15900
+ * every instance, and a v-for over a hundred rows would otherwise print a
15901
+ * hundred identical lines and bury the first one.
15902
+ */
15903
+ static #warnOncePerTag(tagName, message) {
15904
+ const tag = tagName.toLowerCase();
15905
+ if (IchigoElement.#warnedTags.has(tag)) {
15906
+ return;
15907
+ }
15908
+ IchigoElement.#warnedTags.add(tag);
15909
+ console.warn(`[ichigo] <${tag}>: ${message}`);
15910
+ }
15838
15911
  }
15839
15912
 
15840
15913
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.