@mintjamsinc/ichigojs 0.1.81 → 0.1.82

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
@@ -1019,6 +1019,20 @@ defineComponent('my-badge', {
1019
1019
  Use the native `<slot>` element in the component template to project content
1020
1020
  from the parent. ichigo.js components use Light DOM.
1021
1021
 
1022
+ - Slot content is compiled in the **parent scope** and stays the parent's:
1023
+ its bindings, event handlers and the props of components inside it keep
1024
+ working wherever the component places it.
1025
+ - The content is **moved** to the `<slot>`, never copied, so a `<slot>` may sit
1026
+ behind a `v-if` / `v-for` in the component template (e.g. a dialog body shown
1027
+ only while open). Hiding the outlet takes the content out of view; showing it
1028
+ again brings back the same nodes.
1029
+ - Nodes can be in one place only: when the same slot is rendered more than
1030
+ once (a `<slot>` inside a `v-for`), the content goes to the outlet rendered
1031
+ last, and if that outlet goes away it moves to the last one remaining. For
1032
+ content per row, use a scoped slot (below), which renders once per outlet.
1033
+ - A `<slot>` that receives no content renders its own children as fallback, in
1034
+ the component scope.
1035
+
1022
1036
  ### Scoped slots
1023
1037
 
1024
1038
  A scoped slot lets the parent supply a template for content that the component
package/dist/ichigo.cjs CHANGED
@@ -8990,9 +8990,9 @@
8990
8990
  * ```
8991
8991
  *
8992
8992
  * This directive is created only when a scoped template for the slot's name
8993
- * exists (see {@link VSlotOutletDirective.tryCreate}); otherwise `<slot>`
8994
- * elements keep their existing behavior (static distribution at expansion,
8995
- * or fallback content compiled in the component scope).
8993
+ * exists (see {@link VSlotOutletDirective.tryCreate}); otherwise plain slot
8994
+ * content is moved into place by VStaticSlotOutletDirective, and a `<slot>`
8995
+ * with no content at all renders its fallback in the component scope.
8996
8996
  *
8997
8997
  * Rendering happens synchronously in onMount — before the component
8998
8998
  * application would compile the slot's fallback children — by replacing the
@@ -9284,6 +9284,266 @@
9284
9284
  }
9285
9285
  }
9286
9286
 
9287
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9288
+ /**
9289
+ * Renders a static slot outlet: a `<slot>` element inside a component template
9290
+ * whose host element received plain slot content from the parent — child
9291
+ * nodes for the default slot, or elements carrying `slot="name"`:
9292
+ *
9293
+ * ```html
9294
+ * <!-- component template -->
9295
+ * <div v-if="open" class="body"><slot></slot></div>
9296
+ *
9297
+ * <!-- usage -->
9298
+ * <my-dialog :open="show">
9299
+ * <my-field :label="title"></my-field>
9300
+ * </my-dialog>
9301
+ * ```
9302
+ *
9303
+ * The slot content is the parent application's own nodes, compiled in the
9304
+ * parent scope; it keeps its bindings, listeners and component props only as
9305
+ * long as those very nodes are displayed. So the outlet MOVES them into place
9306
+ * and never clones them. That is what lets a `<slot>` sit behind a v-if or
9307
+ * v-for in the component template: the directive clones its template source,
9308
+ * the clone carries a fresh `<slot>`, and the outlet created for that clone
9309
+ * moves the live nodes in.
9310
+ *
9311
+ * Nodes can only be in one place, so when the same slot is rendered more than
9312
+ * once (e.g. a `<slot>` inside a v-for), the outlet rendered last receives the
9313
+ * content and the earlier ones are left empty. When the outlet holding the
9314
+ * content goes away while others remain (its row is removed), the content
9315
+ * moves to the last remaining one. For content per row, use a scoped slot
9316
+ * (`<template v-slot:name="scope">`), which renders a copy per outlet
9317
+ * (VSlotOutletDirective).
9318
+ *
9319
+ * The component application never compiles the moved nodes: they are placed
9320
+ * in onMount, after the parent VNode has taken its snapshot of the children
9321
+ * to compile. Components inside them belong to the parent application, which
9322
+ * expands them through {@link expandComponents} once they are in the document.
9323
+ * When the last outlet goes away (its v-if turns false) the nodes leave with
9324
+ * the removed clone and wait there until the next outlet moves them again;
9325
+ * they are not destroyed, because they are not this application's to destroy.
9326
+ *
9327
+ * Created only when the host captured content for the slot's name and no
9328
+ * scoped template claims it; otherwise the `<slot>` element renders its
9329
+ * fallback children in the component scope.
9330
+ */
9331
+ class VStaticSlotOutletDirective {
9332
+ /**
9333
+ * The live outlets of each slot, in the order they were rendered, keyed by
9334
+ * the slot's node array (one per host element and slot name). The last
9335
+ * one holds the content.
9336
+ */
9337
+ static #outlets = new WeakMap();
9338
+ /**
9339
+ * Marks where the `<slot>` element was, so the content can be moved back
9340
+ * here when a later outlet of the same slot goes away.
9341
+ */
9342
+ #anchor;
9343
+ /**
9344
+ * Guards against double destruction.
9345
+ */
9346
+ #destroyed = false;
9347
+ /**
9348
+ * The virtual node of the `<slot>` element (in the component application).
9349
+ */
9350
+ #vNode;
9351
+ /**
9352
+ * The slot content: the parent application's nodes, shared with the host
9353
+ * element's slot cache.
9354
+ */
9355
+ #nodes;
9356
+ /**
9357
+ * The host element's VNode in the parent application, whose children are
9358
+ * the VNodes of the slot content. Undefined when the host was not compiled
9359
+ * by an application (then there is nothing to expand).
9360
+ */
9361
+ #hostVNode;
9362
+ /**
9363
+ * Creates a static slot outlet for the given `<slot>` VNode when the host
9364
+ * element captured content for its slot name. Returns undefined when there
9365
+ * is none, so the `<slot>` element keeps rendering its fallback content.
9366
+ */
9367
+ static tryCreate(vNode) {
9368
+ const host = vNode.vApplication.hostElement;
9369
+ if (!host || typeof host._ichigoSlotNodes !== 'function') {
9370
+ return undefined;
9371
+ }
9372
+ const element = vNode.node;
9373
+ const nodes = host._ichigoSlotNodes(element.getAttribute('name') || 'default');
9374
+ if (!nodes) {
9375
+ return undefined;
9376
+ }
9377
+ return new VStaticSlotOutletDirective(vNode, nodes, host._ichigoHost?.vNode);
9378
+ }
9379
+ constructor(vNode, nodes, hostVNode) {
9380
+ this.#vNode = vNode;
9381
+ this.#nodes = nodes;
9382
+ this.#hostVNode = hostVNode;
9383
+ // Slot props (v-bind / :) only mean something to a scoped template.
9384
+ // Consume them so the generic attribute parsing does not evaluate them
9385
+ // on an element that is about to leave the document.
9386
+ const element = vNode.node;
9387
+ for (const attr of Array.from(element.attributes)) {
9388
+ if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
9389
+ element.removeAttribute(attr.name);
9390
+ }
9391
+ }
9392
+ }
9393
+ /**
9394
+ * @inheritdoc
9395
+ */
9396
+ get name() {
9397
+ return 'static-slot-outlet';
9398
+ }
9399
+ /**
9400
+ * @inheritdoc
9401
+ */
9402
+ get vNode() {
9403
+ return this.#vNode;
9404
+ }
9405
+ /**
9406
+ * @inheritdoc
9407
+ */
9408
+ get needsAnchor() {
9409
+ return false;
9410
+ }
9411
+ /**
9412
+ * @inheritdoc
9413
+ */
9414
+ get bindingsPreparer() {
9415
+ return undefined;
9416
+ }
9417
+ /**
9418
+ * @inheritdoc
9419
+ */
9420
+ get domUpdater() {
9421
+ return undefined;
9422
+ }
9423
+ /**
9424
+ * @inheritdoc
9425
+ */
9426
+ get templatize() {
9427
+ return false;
9428
+ }
9429
+ /**
9430
+ * @inheritdoc
9431
+ */
9432
+ get dependentIdentifiers() {
9433
+ return [];
9434
+ }
9435
+ /**
9436
+ * @inheritdoc
9437
+ *
9438
+ * Moves the slot content in place of the `<slot>` element — synchronously
9439
+ * during VNode construction, before the component application would
9440
+ * compile the slot's fallback children (which the content replaces).
9441
+ */
9442
+ get onMount() {
9443
+ return () => {
9444
+ const slot = this.#vNode.node;
9445
+ while (slot.firstChild) {
9446
+ slot.removeChild(slot.firstChild);
9447
+ }
9448
+ this.#anchor = document.createComment('slot');
9449
+ slot.replaceWith(this.#anchor);
9450
+ let outlets = VStaticSlotOutletDirective.#outlets.get(this.#nodes);
9451
+ if (!outlets) {
9452
+ outlets = [];
9453
+ VStaticSlotOutletDirective.#outlets.set(this.#nodes, outlets);
9454
+ }
9455
+ outlets.push(this);
9456
+ this.#place();
9457
+ };
9458
+ }
9459
+ /**
9460
+ * @inheritdoc
9461
+ */
9462
+ get onMounted() {
9463
+ return undefined;
9464
+ }
9465
+ /**
9466
+ * @inheritdoc
9467
+ */
9468
+ get onUpdate() {
9469
+ return undefined;
9470
+ }
9471
+ /**
9472
+ * @inheritdoc
9473
+ */
9474
+ get onUpdated() {
9475
+ return undefined;
9476
+ }
9477
+ /**
9478
+ * @inheritdoc
9479
+ */
9480
+ get onUnmount() {
9481
+ return undefined;
9482
+ }
9483
+ /**
9484
+ * @inheritdoc
9485
+ */
9486
+ get onUnmounted() {
9487
+ return undefined;
9488
+ }
9489
+ /**
9490
+ * @inheritdoc
9491
+ *
9492
+ * Expands the components in the slot content through the parent
9493
+ * application's VNodes. Called by the walk of whoever inserted this
9494
+ * outlet into the document (VApplication.mount, v-if, v-for), so the
9495
+ * content is connected by now — unless a later outlet took it, in which
9496
+ * case that outlet's walk expands it.
9497
+ */
9498
+ expandComponents() {
9499
+ const children = this.#hostVNode?.childVNodes;
9500
+ if (!children) {
9501
+ return;
9502
+ }
9503
+ for (const child of children) {
9504
+ if (this.#nodes.includes(child.node)) {
9505
+ child.expandComponents();
9506
+ }
9507
+ }
9508
+ }
9509
+ /**
9510
+ * @inheritdoc
9511
+ *
9512
+ * The slot content is the parent application's; nothing to release here.
9513
+ */
9514
+ destroy() {
9515
+ if (this.#destroyed) {
9516
+ return;
9517
+ }
9518
+ this.#destroyed = true;
9519
+ const outlets = VStaticSlotOutletDirective.#outlets.get(this.#nodes);
9520
+ if (!outlets) {
9521
+ return;
9522
+ }
9523
+ const index = outlets.indexOf(this);
9524
+ if (index === -1) {
9525
+ return;
9526
+ }
9527
+ const wasHolder = index === outlets.length - 1;
9528
+ outlets.splice(index, 1);
9529
+ // Hand the content to the last remaining outlet before this one's DOM
9530
+ // is removed (v-if / v-for destroy the VNode first, then remove it),
9531
+ // so removing the row that shows it does not take it out of view.
9532
+ const next = outlets[outlets.length - 1];
9533
+ if (wasHolder && next) {
9534
+ next.#place();
9535
+ next.expandComponents();
9536
+ }
9537
+ }
9538
+ /**
9539
+ * Moves the slot content right after this outlet's anchor. Moving, not
9540
+ * cloning: nodes shown by another outlet of the same slot leave it.
9541
+ */
9542
+ #place() {
9543
+ this.#anchor?.after(...this.#nodes);
9544
+ }
9545
+ }
9546
+
9287
9547
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9288
9548
  /**
9289
9549
  * Manages directives associated with a virtual node (VNode).
@@ -9452,10 +9712,12 @@
9452
9712
  const element = this.#vNode.node;
9453
9713
  // Parse directives from attributes
9454
9714
  const directives = [];
9455
- // Scoped slot outlet: a <slot> element (without structural directives)
9456
- // inside a component whose host received a matching <template v-slot:...>.
9457
- // This is an element-based directive, created before the generic
9458
- // attribute parsing so it can consume the slot-prop bindings (:x).
9715
+ // Slot outlet: a <slot> element (without structural directives) inside a
9716
+ // component whose host received content for it — a matching
9717
+ // <template v-slot:...> (rendered per outlet), or else plain slot
9718
+ // content (moved into place). This is an element-based directive,
9719
+ // created before the generic attribute parsing so it can consume the
9720
+ // slot-prop bindings (:x).
9459
9721
  // A <slot> carrying v-if / v-for is handled structurally first; the
9460
9722
  // outlet is then created when the clone is compiled.
9461
9723
  if (element.tagName === 'SLOT' &&
@@ -9463,7 +9725,8 @@
9463
9725
  !element.hasAttribute(StandardDirectiveName.V_IF) &&
9464
9726
  !element.hasAttribute(StandardDirectiveName.V_ELSE_IF) &&
9465
9727
  !element.hasAttribute(StandardDirectiveName.V_ELSE)) {
9466
- const outlet = VSlotOutletDirective.tryCreate(this.#vNode);
9728
+ const outlet = VSlotOutletDirective.tryCreate(this.#vNode)
9729
+ ?? VStaticSlotOutletDirective.tryCreate(this.#vNode);
9467
9730
  if (outlet) {
9468
9731
  directives.push(outlet);
9469
9732
  }
@@ -15510,9 +15773,13 @@
15510
15773
  * microtask fires, so data() can read them. A component used without any prop
15511
15774
  * binding still mounts — with its declared defaults — instead of waiting forever.
15512
15775
  *
15513
- * Slot content is captured on the first connection and cached, so a component
15514
- * that is moved in the DOM (e.g. reordered by v-for) re-expands its template
15515
- * and redistributes the same slot nodes on reconnection.
15776
+ * Slot content is captured on the first expansion and cached. The nodes are the
15777
+ * parent application's compiled nodes and are only ever moved, never cloned:
15778
+ * each `<slot>` outlet the component renders moves them into place
15779
+ * (VStaticSlotOutletDirective; scoped `<template v-slot>` content is rendered
15780
+ * per outlet by VSlotOutletDirective instead). A component that is moved in
15781
+ * the DOM (e.g. reordered by v-for) re-expands its template, and its outlets
15782
+ * move the same nodes into the fresh expansion.
15516
15783
  *
15517
15784
  * Subclasses must set the static fields _template, _props, _propOptions and
15518
15785
  * _buildOptions before calling customElements.define(). defineComponent()
@@ -15537,12 +15804,13 @@
15537
15804
  */
15538
15805
  #mountScheduled = false;
15539
15806
  /**
15540
- * Slot content captured on the first connection. Reused on reconnection
15807
+ * Slot content captured on the first expansion. Reused on reconnection
15541
15808
  * (v-for reorder moves the host element, which re-runs connectedCallback
15542
15809
  * with the previously expanded template as children — not authored slot
15543
- * content — so the original capture must be redistributed instead).
15544
- * `scopedSlots` holds `<template v-slot:...>` children by slot name; they
15545
- * are rendered per outlet by VSlotOutletDirective, not distributed here.
15810
+ * content — so the original capture must be used instead).
15811
+ * `defaultNodes` / `namedNodes` are moved into place by
15812
+ * VStaticSlotOutletDirective; `scopedSlots` holds `<template v-slot:...>`
15813
+ * children by slot name, rendered per outlet by VSlotOutletDirective.
15546
15814
  */
15547
15815
  #slotCache;
15548
15816
  /**
@@ -15560,6 +15828,18 @@
15560
15828
  get _ichigoScopedSlots() {
15561
15829
  return this.#slotCache?.scopedSlots;
15562
15830
  }
15831
+ /**
15832
+ * The static slot content captured for the given slot name ('default'
15833
+ * for the unnamed slot): the parent application's nodes, moved into place
15834
+ * by VStaticSlotOutletDirective. Undefined when nothing was provided for
15835
+ * that name, in which case the outlet's fallback content renders.
15836
+ */
15837
+ _ichigoSlotNodes(name) {
15838
+ const nodes = name === 'default'
15839
+ ? this.#slotCache?.defaultNodes
15840
+ : this.#slotCache?.namedNodes.get(name);
15841
+ return nodes && nodes.length > 0 ? nodes : undefined;
15842
+ }
15563
15843
  /**
15564
15844
  * Handles RE-attachment, and nothing else.
15565
15845
  *
@@ -15661,29 +15941,23 @@
15661
15941
  }
15662
15942
  this.#slotCache = { defaultNodes, namedNodes, scopedSlots };
15663
15943
  }
15664
- const { defaultNodes, namedNodes } = this.#slotCache;
15665
15944
  // Clear host element so we can append the cloned template.
15666
15945
  // (On reconnection this discards the previous expansion; the cached slot
15667
- // nodes are still referenced and are moved into the fresh clone below.)
15946
+ // nodes are still referenced and are moved into the fresh expansion by
15947
+ // its outlets.)
15948
+ // The slot nodes are NOT distributed into the template here: they are
15949
+ // the parent application's live, compiled nodes, and a <slot> behind a
15950
+ // v-if / v-for in the template is cloned with its surroundings — which
15951
+ // would render dead copies without their bindings, listeners or props.
15952
+ // They stay parked in the cache until a <slot> outlet is rendered, and
15953
+ // VStaticSlotOutletDirective then moves them into place.
15668
15954
  while (this.firstChild) {
15669
15955
  this.removeChild(this.firstChild);
15670
15956
  }
15671
15957
  // --- 2. Clone the component template ---
15672
15958
  const fragment = templateEl.content.cloneNode(true);
15673
15959
  const root = this.#findRootElement(fragment);
15674
- // --- 3. Distribute named slot content ---
15675
- for (const [name, nodes] of namedNodes) {
15676
- const slot = root.querySelector(`slot[name="${name}"]`);
15677
- if (slot) {
15678
- slot.replaceWith(...nodes);
15679
- }
15680
- }
15681
- // --- 4. Distribute default slot content ---
15682
- const defaultSlot = root.querySelector('slot:not([name])');
15683
- if (defaultSlot && defaultNodes.length > 0) {
15684
- defaultSlot.replaceWith(...defaultNodes);
15685
- }
15686
- // Attach the populated template to the host element
15960
+ // Attach the template to the host element
15687
15961
  this.appendChild(root);
15688
15962
  this.#mountRoot = root;
15689
15963
  this._ichigoExpanded = true;