@mintjamsinc/ichigojs 0.1.79 → 0.1.81

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 CHANGED
@@ -957,6 +957,18 @@ defineComponent('my-list', {
957
957
  </my-list>
958
958
  ```
959
959
 
960
+ > **Ordering:** a component's `<template>` must be in the document, and
961
+ > `defineComponent()` must have run, before the application that uses the
962
+ > component is mounted. The application expands a component while it compiles
963
+ > it — after installing the host context, delivering the prop values and
964
+ > compiling the projected slot content in the parent scope — so a component
965
+ > compiled without its template is reported as a console warning and left
966
+ > empty. It is never retried: an app that renders anyway is an app whose bug
967
+ > cannot be found. A hyphenated tag that nothing has registered (a forgotten
968
+ > import, a typo) is reported the same way. Components therefore have to live
969
+ > inside a mounted application; one placed outside any application is never
970
+ > expanded.
971
+
960
972
  **Props:**
961
973
 
962
974
  - Declared via the `props` array, or as a record with per-prop options:
package/dist/ichigo.cjs CHANGED
@@ -9224,8 +9224,9 @@
9224
9224
  this.#scopeBindings.setLocal(this.#entry.scopeVar, this.#buildSlotProps());
9225
9225
  }
9226
9226
  // Clone the scoped template's content and create its VNode tree BEFORE
9227
- // inserting into the DOM, so components inside the slot content are
9228
- // compiled before their connectedCallback can expand them.
9227
+ // inserting into the DOM, so its directives run against a subtree
9228
+ // nothing else can observe yet. Components inside are expanded
9229
+ // afterwards, once the content is in the document.
9229
9230
  // (Created through the parent application to keep the VNode import
9230
9231
  // type-only; a direct `new VNode` here would close a module cycle
9231
9232
  // VNode -> VDirectiveManager -> VSlotOutletDirective -> VNode.)
@@ -9244,6 +9245,17 @@
9244
9245
  slot.replaceWith(fragment);
9245
9246
  // Initial rendering
9246
9247
  this.#contentVNode.forceUpdate();
9248
+ // Components in the slot content expand once it is in the document:
9249
+ // now, if the outlet is connected, or — when it renders inside a
9250
+ // detached v-if / v-for clone — through expandComponents() below, when
9251
+ // the directive that inserts the clone walks it.
9252
+ this.#contentVNode.expandComponents();
9253
+ }
9254
+ /**
9255
+ * @inheritdoc
9256
+ */
9257
+ expandComponents() {
9258
+ this.#contentVNode?.expandComponents();
9247
9259
  }
9248
9260
  /**
9249
9261
  * Re-evaluates the slot props (component-side change) and pushes them into
@@ -9860,6 +9872,37 @@
9860
9872
  * remove the entire fragment atomically.
9861
9873
  */
9862
9874
  #fragmentRange;
9875
+ /**
9876
+ * Set by {@link destroy}. A destroyed clone stays in its parent's child
9877
+ * list (v-if / v-for render new clones under the same parent), so walks
9878
+ * over the children must skip it.
9879
+ */
9880
+ #destroyed = false;
9881
+ /**
9882
+ * Custom element tags already reported by
9883
+ * {@link VNode.#warnIfUnknownCustomElement}.
9884
+ */
9885
+ static #warnedUnknownTags = new Set();
9886
+ /**
9887
+ * Warns about a hyphenated tag that no one has registered — a forgotten
9888
+ * import, a missing defineComponent() call, or a typo in the tag name.
9889
+ * Such an element is compiled as an ordinary element and renders its own
9890
+ * children, which looks enough like a working page to cost an afternoon.
9891
+ * Reported once per tag; third-party custom elements are recognised
9892
+ * through the registry the browser keeps.
9893
+ */
9894
+ static #warnIfUnknownCustomElement(nodeName) {
9895
+ const tag = nodeName.toLowerCase();
9896
+ if (!tag.includes('-') || VNode.#warnedUnknownTags.has(tag)) {
9897
+ return;
9898
+ }
9899
+ if (typeof customElements !== 'undefined' && customElements.get(tag)) {
9900
+ return;
9901
+ }
9902
+ VNode.#warnedUnknownTags.add(tag);
9903
+ console.warn(`[ichigo] <${tag}>: unknown custom element — nothing has registered this tag.`
9904
+ + ' Did you forget to import its module or call defineComponent(), or is the tag misspelled?');
9905
+ }
9863
9906
  /**
9864
9907
  * Creates an instance of the virtual node.
9865
9908
  * @param args The initialization arguments for the virtual node.
@@ -9891,6 +9934,9 @@
9891
9934
  if (isComponent) {
9892
9935
  element._ichigoHost = { vNode: this };
9893
9936
  }
9937
+ else {
9938
+ VNode.#warnIfUnknownCustomElement(this.#nodeName);
9939
+ }
9894
9940
  // Initialize child virtual nodes
9895
9941
  this.#childVNodes = [];
9896
9942
  // Initialize directive manager
@@ -9924,16 +9970,6 @@
9924
9970
  });
9925
9971
  }
9926
9972
  }
9927
- // A component element this application compiled but that has not
9928
- // expanded yet — it deferred to us because an ancestor component
9929
- // was attaching its template, or its <template> was not loaded when
9930
- // it connected — is expanded now: the host context is installed,
9931
- // the directives above delivered the prop values, and any authored
9932
- // slot content was compiled in this scope. A template source
9933
- // (v-if / v-for) is left alone; its clones expand on insertion.
9934
- if (isComponent && !this.#templatized && !isExpandedComponent) {
9935
- element._ichigoTryExpand?.();
9936
- }
9937
9973
  // After creating child nodes, call onMounted for directives that do not templatize
9938
9974
  if (!this.#templatized) {
9939
9975
  // animation frame to ensure DOM is updated
@@ -9959,6 +9995,58 @@
9959
9995
  // Register this node as a dependent of the parent node, if any
9960
9996
  this.#closers = this.#parentVNode?.addDependent(this);
9961
9997
  }
9998
+ /**
9999
+ * Expands the component elements this application compiled in this subtree
10000
+ * but has not expanded yet, parents before their children.
10001
+ *
10002
+ * This is the ONLY thing that expands a component. It is deliberately a
10003
+ * separate step from compilation rather than a call at the end of the
10004
+ * constructor: a v-if / v-for clone is compiled while it is still detached
10005
+ * (so that its directives run before anything can observe a half-built
10006
+ * subtree), and expanding a detached element would mount a component
10007
+ * application outside the document. The callers therefore compile, insert,
10008
+ * and then call this — see VApplication.mount, VConditionalDirective,
10009
+ * VForDirective and VSlotOutletDirective.
10010
+ *
10011
+ * Walking only {@link childVNodes} keeps the traversal inside this
10012
+ * application: the internals of an expanded component were never compiled
10013
+ * here, so they are not reachable and are left to their own application.
10014
+ * The one exception is content a directive rendered outside its own
10015
+ * children — scoped slot content — reached through
10016
+ * {@link VDirective.expandComponents}.
10017
+ *
10018
+ * Only connected elements are expanded. v-if / v-for call this right after
10019
+ * inserting a clone, but when that clone sits inside another clone that is
10020
+ * still detached (nested directives, or slot content rendered by an outlet
10021
+ * inside a component's v-if), the insertion is not into the document yet;
10022
+ * the outer inserter's walk reaches the same nodes once it is. A
10023
+ * templatized node (a v-if / v-for source) is not expanded itself, but its
10024
+ * rendered clones are its children and are walked.
10025
+ */
10026
+ expandComponents() {
10027
+ if (this.#destroyed) {
10028
+ return;
10029
+ }
10030
+ // Expand only a connected element. A subtree compiled and inserted
10031
+ // into a still-detached clone (a v-if / v-for nested in another
10032
+ // clone, or in scoped slot content rendered inside one) is reached
10033
+ // again by the walk of whoever inserts that clone into the document —
10034
+ // and by then its elements are upgraded and ready to expand.
10035
+ // A templatized node is a template source: never expanded itself,
10036
+ // but its rendered clones are its child VNodes and are walked below.
10037
+ if (!this.#templatized && this.#nodeType === Node.ELEMENT_NODE && IchigoElementRegistry.has(this.#nodeName)) {
10038
+ const element = this.#node;
10039
+ if (element._ichigoExpanded !== true && element.isConnected) {
10040
+ element._ichigoExpand?.();
10041
+ }
10042
+ }
10043
+ // Subtrees a directive rendered outside this VNode's children (the
10044
+ // scoped slot content of a <slot> outlet) are expanded here too: this
10045
+ // walk runs once the subtree is in the document, which the outlet
10046
+ // cannot know when it renders inside a detached v-if / v-for clone.
10047
+ this.#directiveManager?.directives?.forEach(d => d.expandComponents?.());
10048
+ this.#childVNodes?.forEach(child => child.expandComponents());
10049
+ }
9962
10050
  /**
9963
10051
  * The application instance associated with this virtual node.
9964
10052
  */
@@ -10366,6 +10454,7 @@
10366
10454
  * 6. Call onUnmounted lifecycle hooks
10367
10455
  */
10368
10456
  destroy() {
10457
+ this.#destroyed = true;
10369
10458
  // If no directive requires template preservation, call onUnmount for directives that do not templatize
10370
10459
  if (!this.#templatized) {
10371
10460
  this.#directiveManager?.directives?.forEach(d => {
@@ -10759,11 +10848,10 @@
10759
10848
  }
10760
10849
  // Clone the original node and create a new VNode for it
10761
10850
  const clone = this.#cloneNode();
10762
- // Create a new VNode for the cloned element BEFORE inserting into DOM.
10763
- // This prevents custom elements (Web Components) from having their
10764
- // connectedCallback fire before VNode processes their children, which
10765
- // would cause the parent VApplication to incorrectly adopt the custom
10766
- // element's internal template content as its own VNode tree.
10851
+ // Compile the clone BEFORE inserting it, so its directives run against a
10852
+ // subtree nothing else can observe yet (v-if inside creates anchors and
10853
+ // detaches template sources). Components inside are expanded afterwards,
10854
+ // once the clone is in the document — see expandComponents() below.
10767
10855
  // Pass the current bindings to ensure loop variables from v-for are available
10768
10856
  const vNode = new VNode({
10769
10857
  node: clone,
@@ -10778,12 +10866,16 @@
10778
10866
  vNode.fragmentRange = VFragmentRange.insert(anchorParent, nextSibling, 'vif-fragment', clone);
10779
10867
  this.#renderedVNode = vNode;
10780
10868
  this.#renderedVNode.forceUpdate();
10869
+ this.#renderedVNode.expandComponents();
10781
10870
  return;
10782
10871
  }
10783
10872
  const nodeToInsert = vNode.anchorNode || clone;
10784
10873
  anchorParent?.insertBefore(nodeToInsert, nextSibling);
10785
10874
  this.#renderedVNode = vNode;
10786
10875
  this.#renderedVNode.forceUpdate();
10876
+ // Components expand once the clone is in the document and its bindings
10877
+ // have been applied, so they mount connected and with their props set.
10878
+ this.#renderedVNode.expandComponents();
10787
10879
  }
10788
10880
  /**
10789
10881
  * Removes the node from the DOM.
@@ -11240,6 +11332,7 @@
11240
11332
  vNode.fragmentRange = range;
11241
11333
  newRenderedItems.push({ key, vNode });
11242
11334
  vNode.forceUpdate();
11335
+ vNode.expandComponents();
11243
11336
  prevNode = range.lastNode;
11244
11337
  continue;
11245
11338
  }
@@ -11254,6 +11347,10 @@
11254
11347
  }
11255
11348
  newRenderedItems.push({ key, vNode });
11256
11349
  vNode.forceUpdate();
11350
+ // Components expand once the clone is in the document and its
11351
+ // bindings have been applied, so they mount connected and with
11352
+ // their props set.
11353
+ vNode.expandComponents();
11257
11354
  }
11258
11355
  else {
11259
11356
  // Reuse existing item
@@ -14823,6 +14920,10 @@
14823
14920
  });
14824
14921
  // Initial rendering
14825
14922
  this.#vNode.update();
14923
+ // Expand the components in the tree. Last, so every component element
14924
+ // has been compiled and has had its prop values delivered before its
14925
+ // template is cloned in. See VNode.expandComponents.
14926
+ this.#vNode.expandComponents();
14826
14927
  // Remove v-cloak attributes after mounting
14827
14928
  // v-cloak is used to hide un-compiled template until the app is ready
14828
14929
  // Usage: Add CSS rule "[v-cloak] { display: none; }" to hide elements with v-cloak
@@ -15387,6 +15488,19 @@
15387
15488
  /**
15388
15489
  * Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
15389
15490
  *
15491
+ * Expansion — cloning the template into this element — is driven by the
15492
+ * application that owns the element, not by connectedCallback: VNode compiles
15493
+ * the element first (installing the host context, delivering the prop values
15494
+ * and compiling the authored slot content in the owner's scope) and only then
15495
+ * calls {@link _ichigoExpand}. That leaves connectedCallback a single job,
15496
+ * re-attachment; see there.
15497
+ *
15498
+ * The component's <template> must be present in the document, and
15499
+ * defineComponent() must have run, before the owning application is mounted.
15500
+ * That is the application's responsibility. A missing template is reported as
15501
+ * a warning and the element is left empty — it is never silently retried,
15502
+ * because an app that renders anyway is an app whose bug cannot be found.
15503
+ *
15390
15504
  * Mount timing:
15391
15505
  * - If the component declares no props, the VApplication is mounted synchronously
15392
15506
  * at the end of connectedCallback (the template is known, no props to wait for).
@@ -15446,38 +15560,42 @@
15446
15560
  get _ichigoScopedSlots() {
15447
15561
  return this.#slotCache?.scopedSlots;
15448
15562
  }
15563
+ /**
15564
+ * Handles RE-attachment, and nothing else.
15565
+ *
15566
+ * A first connection is not what expands this element: the owning
15567
+ * application does that from {@link _ichigoExpand} once it has compiled the
15568
+ * element. So a connection event on a not-yet-expanded element means the
15569
+ * owner has not reached it yet, and the right thing to do is nothing. That
15570
+ * covers every case the old guards enumerated one by one — a v-if / v-for
15571
+ * template source, an element inside an enclosing component's template
15572
+ * clone, a clone inserted before its directive expands it.
15573
+ *
15574
+ * An expanded element whose mount root has been torn down, on the other
15575
+ * hand, was disconnected and is now coming back: v-for reorders relocate
15576
+ * the host element, which disconnects and reconnects it. Its children are
15577
+ * the previous expansion, not authored slot content, so it re-expands from
15578
+ * the cached slot nodes. Testing the mount root rather than the expansion
15579
+ * flag alone keeps this from firing on an element that was expanded while
15580
+ * detached and is being inserted for the first time.
15581
+ */
15449
15582
  connectedCallback() {
15450
- // --- 0a. Guard: do not expand a template-source element ---
15451
- // An element still carrying a templatizing directive attribute (v-if
15452
- // family / v-for) is a template source: the owning application clones it
15453
- // and inserts the clones (whose directive attributes are stripped during
15454
- // clone compilation, so the clones expand normally). Expanding the source
15455
- // here would bake the expansion into every clone, and the clone's
15456
- // internals would then be compiled in the parent scope.
15457
- if (this.hasAttribute(StandardDirectiveName.V_IF) ||
15458
- this.hasAttribute(StandardDirectiveName.V_ELSE_IF) ||
15459
- this.hasAttribute(StandardDirectiveName.V_ELSE) ||
15460
- this.hasAttribute(StandardDirectiveName.V_FOR)) {
15461
- return;
15462
- }
15463
- // --- 0a2. Guard: wait to be compiled by the owning application ---
15464
- // This element came from an enclosing component's template clone and no
15465
- // application has compiled it yet: the enclosing component's appendChild
15466
- // connected it before that component's VApplication existed. Expanding
15467
- // now would capture the slot content and mount before the owning
15468
- // application installs the host context (_ichigoHost) and delivers the
15469
- // prop values — scoped slot outlets would find no parent scope and
15470
- // silently fall back to their default content. The owning application
15471
- // expands this element while it compiles it (VNode ->
15472
- // _ichigoTryExpand()), by which time both are in place.
15473
- //
15474
- // Only template-owned elements defer: authored slot content placed
15475
- // inside a component by an application is not part of the template, so
15476
- // it keeps expanding immediately (and is redistributed by the enclosing
15477
- // component on every re-expansion).
15478
- if (this._ichigoTemplateOwned === true && !this._ichigoHost) {
15479
- return;
15583
+ if (this._ichigoExpanded && !this.#mountRoot) {
15584
+ this._ichigoExpand();
15480
15585
  }
15586
+ }
15587
+ /**
15588
+ * Expands this element's template. Called by the application that owns the
15589
+ * element, right after it compiled it (VNode.expandComponents): the host
15590
+ * context is installed, the prop bindings have delivered their values and
15591
+ * the authored slot content has been compiled in the owner's scope, so the
15592
+ * capture below sees owner-compiled nodes and the component's own
15593
+ * application starts with its props already in hand.
15594
+ *
15595
+ * Also called by {@link connectedCallback} to rebuild the expansion after a
15596
+ * move.
15597
+ */
15598
+ _ichigoExpand() {
15481
15599
  // --- 0b. Reclaim prop values assigned before upgrade ---
15482
15600
  // A plain property set on the instance (e.g. by a binding delivered
15483
15601
  // while the element was detached or not yet upgraded) is an own data
@@ -15493,13 +15611,15 @@
15493
15611
  this[prop] = value;
15494
15612
  }
15495
15613
  }
15496
- // --- 0c. Guard: skip if template not yet loaded ---
15497
- // customElements.define() may run before loadComponent() completes (e.g. when
15498
- // dynamic imports are inlined by the bundler). The static placeholder element
15499
- // in the HTML will be removed and replaced by v-for once the template is ready,
15500
- // so it is safe to do nothing here.
15614
+ // --- 0c. Resolve the template ---
15615
+ // Having the <template> in the document by now is the application's
15616
+ // contract (see the class doc). Report the breach and leave the element
15617
+ // empty: rendering something plausible here would hide the bug at the
15618
+ // one moment it is still cheap to find.
15501
15619
  const templateEl = document.querySelector(ctor._template);
15502
15620
  if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
15621
+ IchigoElement.#warnOncePerTag(this.tagName, `template '${ctor._template}' was not found in the document. `
15622
+ + 'Inject the <template> and call defineComponent() before mounting the application.');
15503
15623
  return;
15504
15624
  }
15505
15625
  // --- 1. Capture slot content (first connection only) ---
@@ -15551,18 +15671,6 @@
15551
15671
  // --- 2. Clone the component template ---
15552
15672
  const fragment = templateEl.content.cloneNode(true);
15553
15673
  const root = this.#findRootElement(fragment);
15554
- // Mark the component elements the template brought in, so they let this
15555
- // component's application compile them before they expand (guard 0a2).
15556
- // Marked before slot distribution, so authored slot content — which is
15557
- // not part of the template — keeps expanding under the application that
15558
- // authored it. Elements inside a nested <template> are not marked: they
15559
- // are inert until a directive clones them, and the clone is compiled
15560
- // before it is inserted.
15561
- for (const element of Array.from(fragment.querySelectorAll('*'))) {
15562
- if (IchigoElementRegistry.has(element.tagName)) {
15563
- element._ichigoTemplateOwned = true;
15564
- }
15565
- }
15566
15674
  // --- 3. Distribute named slot content ---
15567
15675
  for (const [name, nodes] of namedNodes) {
15568
15676
  const slot = root.querySelector(`slot[name="${name}"]`);
@@ -15600,21 +15708,6 @@
15600
15708
  this.#app = undefined;
15601
15709
  }
15602
15710
  }
15603
- /**
15604
- * Expands this element's template on behalf of the application that owns
15605
- * it. Called by VNode right after it compiled this element — the host
15606
- * context is installed and the prop bindings have delivered their values,
15607
- * so a component that deferred its expansion (guard 0a2, or a template
15608
- * that was not loaded when it connected) can now expand in the right
15609
- * order. A no-op when the element is already expanded or not connected
15610
- * (a template-source clone expands when it is inserted).
15611
- */
15612
- _ichigoTryExpand() {
15613
- if (this._ichigoExpanded || !this.isConnected) {
15614
- return;
15615
- }
15616
- this.connectedCallback();
15617
- }
15618
15711
  /**
15619
15712
  * Called by the property setters generated by defineComponent().
15620
15713
  * Before mount: stores the value and schedules a mount microtask.
@@ -15841,6 +15934,23 @@
15841
15934
  #warn(message) {
15842
15935
  console.warn(`[ichigo] <${this.tagName.toLowerCase()}>: ${message}`);
15843
15936
  }
15937
+ /**
15938
+ * Tags already reported by {@link #warnOncePerTag}.
15939
+ */
15940
+ static #warnedTags = new Set();
15941
+ /**
15942
+ * Reports a per-component-type problem once. A missing template breaks
15943
+ * every instance, and a v-for over a hundred rows would otherwise print a
15944
+ * hundred identical lines and bury the first one.
15945
+ */
15946
+ static #warnOncePerTag(tagName, message) {
15947
+ const tag = tagName.toLowerCase();
15948
+ if (IchigoElement.#warnedTags.has(tag)) {
15949
+ return;
15950
+ }
15951
+ IchigoElement.#warnedTags.add(tag);
15952
+ console.warn(`[ichigo] <${tag}>: ${message}`);
15953
+ }
15844
15954
  }
15845
15955
 
15846
15956
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.