@xaendar/core 0.6.20 → 0.7.0

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.
@@ -85,7 +85,7 @@ export declare type BaseWebComponentConstructor = Constructor<BaseWebComponent,
85
85
  */
86
86
  declare type Block = {
87
87
  condition?: NoArgsFunction<boolean>;
88
- block: Function_2<[HTMLElement, Context], Context>;
88
+ block: Function_2<[HTMLElement, Context, Node | null], Context>;
89
89
  };
90
90
 
91
91
  /**
@@ -131,7 +131,16 @@ export declare class Context {
131
131
  */
132
132
  private _children;
133
133
  /**
134
- * DOM nodes owned by this context, tracked for cleanup on destruction.
134
+ * Map containing the run time values of variables defines during the run time execution
135
+ * (e.g. {@for} variables: 'index', 'item', etc...)
136
+ */
137
+ private _variables;
138
+ /**
139
+ * DOM nodes directly mounted into this scope via {@link mountNode} (does not
140
+ * include nodes owned by child contexts). Used to determine, for a given
141
+ * context, which DOM nodes belong to it — for example so `_for`'s keyed
142
+ * diffing can move a reused item's nodes to a new position without
143
+ * recreating them.
135
144
  */
136
145
  private _nodes;
137
146
  /**
@@ -144,10 +153,24 @@ export declare class Context {
144
153
  * Creates a new scope context.
145
154
  *
146
155
  * @param _root - Web component reference used to resolve property and method bindings.
147
- * @param _parent - Optional parent context representing the enclosing scope.
148
- * @param _identifiers - Named identifier bindings declared in this scope.
149
156
  */
150
157
  constructor(_root: BaseWebComponent, _parent: Context);
158
+ /**
159
+ * Registers a new identifier name in this scope.
160
+ *
161
+ * @param name - The identifier name to register.
162
+ * @throws When an identifier with the same name is already declared in this scope.
163
+ */
164
+ addIdentifier(name: string, value: unknown): void;
165
+ removeIdentifier(name: string): void;
166
+ /**
167
+ * Returns `true` if an identifier with the given name is declared in this
168
+ * scope or any of its ancestor scopes.
169
+ *
170
+ * @param name - The identifier name to look up.
171
+ * @returns `true` if the identifier exists in the scope chain, `false` otherwise.
172
+ */
173
+ get(name: string): unknown;
151
174
  /**
152
175
  * Returns the event handler method bound to the root component instance.
153
176
  *
@@ -168,7 +191,7 @@ export declare class Context {
168
191
  */
169
192
  removeChild(context: Context): void;
170
193
  /**
171
- * Registers a DOM node as owned by this context.
194
+ * Registers a DOM node as directly owned by this context.
172
195
  *
173
196
  * @param node - The DOM node to track.
174
197
  */
@@ -179,6 +202,13 @@ export declare class Context {
179
202
  * @param nodeToRemove - The DOM node to untrack.
180
203
  */
181
204
  removeNode(nodeToRemove: Node): void;
205
+ /**
206
+ * Returns the DOM nodes directly owned by this context (not recursively
207
+ * including nodes owned by child contexts), in mount order. Used by
208
+ * `_for`'s keyed diffing to locate a reused item's nodes so they can be
209
+ * repositioned with `insertBefore` instead of being destroyed and recreated.
210
+ */
211
+ getNodes(): ReadonlyArray<Node>;
182
212
  /**
183
213
  * Registers one or more cleanup functions to be called when this context is destroyed.
184
214
  *
@@ -198,6 +228,25 @@ export declare class Context {
198
228
  unlisten(): void;
199
229
  }
200
230
 
231
+ /**
232
+ * Creates a Comment node used as a positional placeholder for dynamic content
233
+ * (`_if`, `_for`). It is inserted immediately, in order, and stays in place for
234
+ * the entire lifetime of the given `context`: any future insertion of dynamic
235
+ * content is done via `insertBefore(node, anchor)`, guaranteeing the correct
236
+ * position even when sibling constructs update independently and asynchronously.
237
+ *
238
+ * @param label - Textual label for the comment, useful for debugging to visually
239
+ * distinguish in the DOM which construct (if/for) generated the anchor.
240
+ * @param parentNode - The parent HTML element the anchor is inserted into.
241
+ * @param context - The Context the anchor is bound to: it determines the anchor's
242
+ * lifecycle, since it will be removed from the DOM when this context is destroyed.
243
+ * @param referenceNode - The node to insert the anchor relative to (same semantics
244
+ * as `Node.insertBefore`). If `null`, the anchor is appended at the end of `parentNode`.
245
+ * @returns The created Comment node, to be used as the reference point for future
246
+ * insertions of dynamic content via `insertBefore(node, anchor)`.
247
+ */
248
+ export declare function createAnchor(label: string, parentNode: HTMLElement, context: Context, referenceNode?: Comment | null): Comment;
249
+
201
250
  /**
202
251
  * Runs a side-effectful function and automatically re-runs it whenever any
203
252
  * Signal read during its execution changes.
@@ -267,16 +316,27 @@ export { Event_2 as Event }
267
316
  export declare type EventOptions = Beautify<RequireOne<Omit<CustomEventInit, 'detail'>>>;
268
317
 
269
318
  /**
270
- * Reactively iterates over a list of items, re-running the loop whenever the tracked condition changes.
271
- * Previous iteration side-effects are cleaned up before each re-evaluation.
272
- *
273
- * @param parentNode - The parent HTML element where the conditional structure is applied.
274
- * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
275
- * @param condition - A reactive function that returns the array of items to iterate over.
276
- * @param trackExpression - An expression used to identify univocally single iterations to not duplicate DOM elements between different renders
277
- * @param forFn - A callback invoked for each item, receiving the parent node, the item, and its index. Must return an array of cleanup functions.
319
+ * Reactively iterates over a list of items. Items are matched across re-runs
320
+ * by the key produced by `trackExpression`:
321
+ * - key sopravvive → il Context e l'intero sottoalbero (stato, subscription,
322
+ * richieste pendenti) vengono riusati SEMPRE, indipendentemente da un
323
+ * eventuale cambio di indice: si spostano solo i nodi DOM, e se il body usa
324
+ * $index/$first/$last/$even/$odd questi vengono aggiornati in-place tramite
325
+ * `update`, senza ricreare nulla.
326
+ * - key sparita il Context viene distrutto.
327
+ * - key nuova → il Context viene creato.
328
+ *
329
+ * @param forFn - Callback invocata per creare un nuovo item. Deve ritornare
330
+ * sia il Context che possiede i nodi, sia (opzionalmente) una funzione
331
+ * `update` per aggiornare le variabili implicite in-place quando l'item
332
+ * viene riusato a un indice diverso.
278
333
  */
279
- export declare function _for(parentNode: HTMLElement, parentContext: Context, condition: () => unknown[], trackExpression: Function_2<[unknown], string | number>, forFn: Function_2<[parentNode: HTMLElement, context: Context, items: unknown[], index: number], Context>): void;
334
+ export declare function _for(parentNode: HTMLElement, parentContext: Context, condition: () => unknown[], trackExpression: Function_2<[unknown], ForKey>, forFn: Function_2<[HTMLElement, Context, unknown[], number, Node | null], {
335
+ context: Context;
336
+ update?: (newIndex: number, items: unknown[]) => void;
337
+ }>): void;
338
+
339
+ declare type ForKey = string | number;
280
340
 
281
341
  /**
282
342
  * Creates a reactive conditional structure from a list of branches.
@@ -294,9 +354,6 @@ export declare function _for(parentNode: HTMLElement, parentContext: Context, co
294
354
  *
295
355
  * @param parentNode - The parent HTML element where the conditional structure is applied.
296
356
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
297
- * @param unwatchFns - Shared array that collects all active cleanup functions.
298
- * Mutated in place: the created effect and branch functions are added to and
299
- * removed from this array.
300
357
  * @param blocks - Ordered list of conditional branches to evaluate.
301
358
  */
302
359
  export declare function _if(parentNode: HTMLElement, parentContext: Context, blocks: Block[]): void;
@@ -367,34 +424,47 @@ export declare type InputSignalOptions<ActualValue = unknown, IncomingValue = Ac
367
424
 
368
425
  /**
369
426
  * Builds a record of iteration context variables for a given index in the loop.
370
- * Provides the current item, index, and convenience flags (`$first`, `$last`, `$even`, `$odd`).
427
+ * `item` is a plain value (identity-stable across moves thanks to the key),
428
+ * while `$index`/`$first`/`$last`/`$even`/`$odd` are signals: when an item is
429
+ * moved to a different position in the array, `update()` writes the new
430
+ * values into these signals in place, instead of recreating the item's
431
+ * template output. Anything bound to these variables re-runs reactively;
432
+ * everything else in the item's subtree (state, subscriptions, pending
433
+ * requests) is left completely untouched.
371
434
  *
372
435
  * @param items - The full array being iterated.
373
436
  * @param index - The current iteration index.
374
- * @param itemName - The identifier to reference the i-th item during iteration
375
- * @param aliases - Aliases for implicit variables defined in the `{@for} loop`
376
- * @returns A record mapping alias names and built-in variables to their values.
437
+ * @param itemName - The identifier to reference the i-th item during iteration.
438
+ * @param aliases - Aliases for implicit variables defined in the `@for` loop.
439
+ * @returns A handle exposing the resolved variables and an `update` function.
377
440
  */
378
- export declare function _iterationVariables(items: unknown[], index: number, itemName: string, aliases: {
441
+ export declare function _iterationVariables(context: Context, items: unknown[], index: number, itemName: string, aliases: {
379
442
  $index: string;
380
443
  $first: string;
381
444
  $last: string;
382
445
  $even: string;
383
446
  $odd: string;
384
- }): Record<string, unknown>;
447
+ }): IterationVariablesHandle;
448
+
449
+ declare type IterationVariablesHandle = {
450
+ vars: Record<string, unknown>;
451
+ update: (newIndex: number, items: unknown[]) => void;
452
+ };
385
453
 
386
454
  /**
387
455
  * Mounts a node into the DOM and binds it to the context lifecycle.
388
456
  *
389
- * Registers the node with the context, appends it to `parentNode`, and
390
- * schedules a cleanup listener that removes the node from both the context
391
- * and the DOM when the context is destroyed.
457
+ * Registers the node with the context, inserts it into `parentNode` at the
458
+ * given position, and schedules a cleanup listener that removes the node
459
+ * from both the context and the DOM when the context is destroyed.
392
460
  *
393
461
  * @param node - The node to mount.
394
- * @param parentNode - The parent HTML element to append the node to.
462
+ * @param parentNode - The parent HTML element to insert the node into.
395
463
  * @param context - The current template execution scope that owns the node's lifecycle.
464
+ * @param referenceNode - The node to insert relative to (same semantics
465
+ * as `Node.insertBefore`). If `null`, the node is appended at the end of `parentNode`.
396
466
  */
397
- export declare function mountNode(node: Node, parentNode: HTMLElement, context: Context): void;
467
+ export declare function mountNode(node: Node, parentNode: HTMLElement, context: Context, referenceNode?: Comment | null): void;
398
468
 
399
469
  /**
400
470
  * Represents the output type returned by an `@Event` decorator in a web component.
@@ -485,7 +555,7 @@ declare type PropertyDecoratorOptionsWithRequired<ActualValue = unknown, Incomin
485
555
  * @param events - List of event listener descriptors to attach to the element.
486
556
  * @returns The newly created HTML element.
487
557
  */
488
- export declare function _renderElement(parentNode: HTMLElement, context: Context, tagName: string, attributes: RenderElementAttribute[], events: RenderElementEvent[]): HTMLElement;
558
+ export declare function _renderElement(parentNode: HTMLElement, context: Context, anchor: Comment | null, tagName: string, attributes: RenderElementAttribute[], events: RenderElementEvent[]): HTMLElement;
489
559
 
490
560
  /**
491
561
  * Describes a single HTML attribute to be applied to a rendered DOM element.
@@ -606,8 +676,6 @@ export { Signal_2 as Signal }
606
676
  *
607
677
  * @param parentNode - The parent HTML element where the conditional structure is applied.
608
678
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
609
- * @param unwatchFns - Shared array that collects all active cleanup functions.
610
- * Mutated in place by the underlying {@link _if} call.
611
679
  * @param expression - Function that evaluates the switch expression. Called
612
680
  * reactively inside an effect so that signal reads are tracked.
613
681
  * @param blocks - Ordered list of case branches. Each entry contains:
@@ -618,7 +686,7 @@ export { Signal_2 as Signal }
618
686
  */
619
687
  export declare function _switch(parentNode: HTMLElement, parentContext: Context, expression: NoArgsFunction<unknown>, blocks: Array<{
620
688
  condition: unknown[] | null;
621
- block: Function_2<[HTMLElement, Context], Context>;
689
+ block: Function_2<[HTMLElement, Context, Node | null], Context>;
622
690
  }>): void;
623
691
 
624
692
  /**
@@ -507,8 +507,17 @@ var Context = class {
507
507
  * Child contexts representing nested scopes (e.g. `@for` loop iterations, `@if` branches).
508
508
  */
509
509
  _children = new Array();
510
- /**
511
- * DOM nodes owned by this context, tracked for cleanup on destruction.
510
+ /**
511
+ * Map containing the run time values of variables defines during the run time execution
512
+ * (e.g. {@for} variables: 'index', 'item', etc...)
513
+ */
514
+ _variables = /* @__PURE__ */ new Map();
515
+ /**
516
+ * DOM nodes directly mounted into this scope via {@link mountNode} (does not
517
+ * include nodes owned by child contexts). Used to determine, for a given
518
+ * context, which DOM nodes belong to it — for example so `_for`'s keyed
519
+ * diffing can move a reused item's nodes to a new position without
520
+ * recreating them.
512
521
  */
513
522
  _nodes = new Array();
514
523
  /**
@@ -521,14 +530,35 @@ var Context = class {
521
530
  * Creates a new scope context.
522
531
  *
523
532
  * @param _root - Web component reference used to resolve property and method bindings.
524
- * @param _parent - Optional parent context representing the enclosing scope.
525
- * @param _identifiers - Named identifier bindings declared in this scope.
526
533
  */
527
534
  constructor(_root, _parent) {
528
535
  this._root = _root;
529
536
  this._parent = _parent;
530
537
  }
531
538
  /**
539
+ * Registers a new identifier name in this scope.
540
+ *
541
+ * @param name - The identifier name to register.
542
+ * @throws When an identifier with the same name is already declared in this scope.
543
+ */
544
+ addIdentifier(name, value) {
545
+ if (this._variables.has(name)) throw new Error(`Identifier "${name}" is already declared in this scope.`);
546
+ this._variables.set(name, value);
547
+ }
548
+ removeIdentifier(name) {
549
+ this._variables.delete(name);
550
+ }
551
+ /**
552
+ * Returns `true` if an identifier with the given name is declared in this
553
+ * scope or any of its ancestor scopes.
554
+ *
555
+ * @param name - The identifier name to look up.
556
+ * @returns `true` if the identifier exists in the scope chain, `false` otherwise.
557
+ */
558
+ get(name) {
559
+ return this._variables.has(name) ? this._variables.get(name) : this._parent?.get(name);
560
+ }
561
+ /**
532
562
  * Returns the event handler method bound to the root component instance.
533
563
  *
534
564
  * @param handler - The name of the method on the root component to retrieve.
@@ -551,10 +581,10 @@ var Context = class {
551
581
  * @param context - The child context to remove.
552
582
  */
553
583
  removeChild(context) {
554
- this._children = this._children.filter((child) => child === context);
584
+ this._children = this._children.filter((child) => child !== context);
555
585
  }
556
586
  /**
557
- * Registers a DOM node as owned by this context.
587
+ * Registers a DOM node as directly owned by this context.
558
588
  *
559
589
  * @param node - The DOM node to track.
560
590
  */
@@ -567,7 +597,16 @@ var Context = class {
567
597
  * @param nodeToRemove - The DOM node to untrack.
568
598
  */
569
599
  removeNode(nodeToRemove) {
570
- this._nodes = this._nodes.filter((node) => node === nodeToRemove);
600
+ this._nodes = this._nodes.filter((node) => node !== nodeToRemove);
601
+ }
602
+ /**
603
+ * Returns the DOM nodes directly owned by this context (not recursively
604
+ * including nodes owned by child contexts), in mount order. Used by
605
+ * `_for`'s keyed diffing to locate a reused item's nodes so they can be
606
+ * repositioned with `insertBefore` instead of being destroyed and recreated.
607
+ */
608
+ getNodes() {
609
+ return this._nodes;
571
610
  }
572
611
  /**
573
612
  * Registers one or more cleanup functions to be called when this context is destroyed.
@@ -591,79 +630,149 @@ var Context = class {
591
630
  this._unwatchFns.forEach((fn) => fn());
592
631
  this._children.forEach((child) => child.unlisten());
593
632
  this._unwatchFns = [];
633
+ this._children = [];
594
634
  this._nodes = [];
635
+ this._variables.clear();
595
636
  }
596
637
  };
597
638
  /**
598
639
  * Mounts a node into the DOM and binds it to the context lifecycle.
599
640
  *
600
- * Registers the node with the context, appends it to `parentNode`, and
601
- * schedules a cleanup listener that removes the node from both the context
602
- * and the DOM when the context is destroyed.
641
+ * Registers the node with the context, inserts it into `parentNode` at the
642
+ * given position, and schedules a cleanup listener that removes the node
643
+ * from both the context and the DOM when the context is destroyed.
603
644
  *
604
645
  * @param node - The node to mount.
605
- * @param parentNode - The parent HTML element to append the node to.
646
+ * @param parentNode - The parent HTML element to insert the node into.
606
647
  * @param context - The current template execution scope that owns the node's lifecycle.
648
+ * @param referenceNode - The node to insert relative to (same semantics
649
+ * as `Node.insertBefore`). If `null`, the node is appended at the end of `parentNode`.
607
650
  */
608
- function mountNode(node, parentNode, context) {
651
+ function mountNode(node, parentNode, context, referenceNode = null) {
609
652
  context.addNode(node);
610
- parentNode.appendChild(node);
653
+ parentNode.insertBefore(node, referenceNode);
611
654
  context.listen(() => {
612
655
  context.removeNode(node);
613
- parentNode.removeChild(node);
656
+ if (node.parentNode === parentNode) parentNode.removeChild(node);
614
657
  });
615
658
  }
659
+ /**
660
+ * Creates a Comment node used as a positional placeholder for dynamic content
661
+ * (`_if`, `_for`). It is inserted immediately, in order, and stays in place for
662
+ * the entire lifetime of the given `context`: any future insertion of dynamic
663
+ * content is done via `insertBefore(node, anchor)`, guaranteeing the correct
664
+ * position even when sibling constructs update independently and asynchronously.
665
+ *
666
+ * @param label - Textual label for the comment, useful for debugging to visually
667
+ * distinguish in the DOM which construct (if/for) generated the anchor.
668
+ * @param parentNode - The parent HTML element the anchor is inserted into.
669
+ * @param context - The Context the anchor is bound to: it determines the anchor's
670
+ * lifecycle, since it will be removed from the DOM when this context is destroyed.
671
+ * @param referenceNode - The node to insert the anchor relative to (same semantics
672
+ * as `Node.insertBefore`). If `null`, the anchor is appended at the end of `parentNode`.
673
+ * @returns The created Comment node, to be used as the reference point for future
674
+ * insertions of dynamic content via `insertBefore(node, anchor)`.
675
+ */
676
+ function createAnchor(label, parentNode, context, referenceNode = null) {
677
+ const anchor = document.createComment(label);
678
+ mountNode(anchor, parentNode, context, referenceNode);
679
+ return anchor;
680
+ }
616
681
  //#endregion
617
682
  //#region ../packages/core/src/utils/for.util.ts
618
683
  /**
619
- * Reactively iterates over a list of items, re-running the loop whenever the tracked condition changes.
620
- * Previous iteration side-effects are cleaned up before each re-evaluation.
621
- *
622
- * @param parentNode - The parent HTML element where the conditional structure is applied.
623
- * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
624
- * @param condition - A reactive function that returns the array of items to iterate over.
625
- * @param trackExpression - An expression used to identify univocally single iterations to not duplicate DOM elements between different renders
626
- * @param forFn - A callback invoked for each item, receiving the parent node, the item, and its index. Must return an array of cleanup functions.
684
+ * Reactively iterates over a list of items. Items are matched across re-runs
685
+ * by the key produced by `trackExpression`:
686
+ * - key sopravvive → il Context e l'intero sottoalbero (stato, subscription,
687
+ * richieste pendenti) vengono riusati SEMPRE, indipendentemente da un
688
+ * eventuale cambio di indice: si spostano solo i nodi DOM, e se il body usa
689
+ * $index/$first/$last/$even/$odd questi vengono aggiornati in-place tramite
690
+ * `update`, senza ricreare nulla.
691
+ * - key sparita il Context viene distrutto.
692
+ * - key nuova → il Context viene creato.
693
+ *
694
+ * @param forFn - Callback invocata per creare un nuovo item. Deve ritornare
695
+ * sia il Context che possiede i nodi, sia (opzionalmente) una funzione
696
+ * `update` per aggiornare le variabili implicite in-place quando l'item
697
+ * viene riusato a un indice diverso.
627
698
  */
628
699
  function _for(parentNode, parentContext, condition, trackExpression, forFn) {
629
- let contexts = new Array();
700
+ const anchor = createAnchor("for", parentNode, parentContext);
701
+ let entries = /* @__PURE__ */ new Map();
630
702
  const unlistener = effect(() => {
631
- contexts.forEach((context) => {
632
- context.unlisten();
633
- parentContext.removeChild(context);
634
- });
635
- contexts = [];
636
703
  const items = condition();
704
+ const newKeys = items.map((item) => trackExpression(item));
705
+ const newKeySet = new Set(newKeys);
637
706
  untracked(() => {
638
- for (let i = 0; i < items.length; i++) {
639
- const context = forFn(parentNode, parentContext, items, i);
640
- contexts.push(context);
641
- parentContext.addChild(context);
707
+ const newEntries = /* @__PURE__ */ new Map();
708
+ for (const [key, entry] of entries) if (!newKeySet.has(key)) {
709
+ entry.context.unlisten();
710
+ parentContext.removeChild(entry.context);
642
711
  }
712
+ let nextReference = anchor;
713
+ for (let i = items.length - 1; i >= 0; i--) {
714
+ const key = newKeys[i];
715
+ const existing = entries.get(key);
716
+ let entry;
717
+ if (existing) {
718
+ const nodes = existing.context.getNodes();
719
+ if (nodes[nodes.length - 1]?.nextSibling !== nextReference) nodes.forEach((node) => parentNode.insertBefore(node, nextReference));
720
+ existing.update?.(i, items);
721
+ entry = existing;
722
+ } else {
723
+ const created = forFn(parentNode, parentContext, items, i, nextReference);
724
+ parentContext.addChild(created.context);
725
+ entry = created;
726
+ }
727
+ newEntries.set(key, entry);
728
+ nextReference = entry.context.getNodes()[0] ?? nextReference;
729
+ }
730
+ entries = newEntries;
643
731
  });
644
732
  });
645
733
  parentContext.listen(unlistener);
646
734
  }
647
735
  /**
648
736
  * Builds a record of iteration context variables for a given index in the loop.
649
- * Provides the current item, index, and convenience flags (`$first`, `$last`, `$even`, `$odd`).
737
+ * `item` is a plain value (identity-stable across moves thanks to the key),
738
+ * while `$index`/`$first`/`$last`/`$even`/`$odd` are signals: when an item is
739
+ * moved to a different position in the array, `update()` writes the new
740
+ * values into these signals in place, instead of recreating the item's
741
+ * template output. Anything bound to these variables re-runs reactively;
742
+ * everything else in the item's subtree (state, subscriptions, pending
743
+ * requests) is left completely untouched.
650
744
  *
651
745
  * @param items - The full array being iterated.
652
746
  * @param index - The current iteration index.
653
- * @param itemName - The identifier to reference the i-th item during iteration
654
- * @param aliases - Aliases for implicit variables defined in the `{@for} loop`
655
- * @returns A record mapping alias names and built-in variables to their values.
747
+ * @param itemName - The identifier to reference the i-th item during iteration.
748
+ * @param aliases - Aliases for implicit variables defined in the `@for` loop.
749
+ * @returns A handle exposing the resolved variables and an `update` function.
656
750
  */
657
- function _iterationVariables(items, index, itemName, aliases) {
658
- const even = index % 2 === 0;
659
- return {
660
- [itemName]: items[index],
661
- [aliases.$index]: index,
662
- [aliases.$first]: index === 0,
663
- [aliases.$last]: index === items.length - 1,
664
- [aliases.$even]: even,
665
- [aliases.$odd]: !even
751
+ function _iterationVariables(context, items, index, itemName, aliases) {
752
+ const $index = signal(index);
753
+ const $first = signal(index === 0);
754
+ const $last = signal(index === items.length - 1);
755
+ const $even = signal(index % 2 === 0);
756
+ const $odd = signal(index % 2 !== 0);
757
+ const retVal = {
758
+ vars: {
759
+ [itemName]: items[index],
760
+ [aliases.$index]: $index,
761
+ [aliases.$first]: $first,
762
+ [aliases.$last]: $last,
763
+ [aliases.$even]: $even,
764
+ [aliases.$odd]: $odd
765
+ },
766
+ update(newIndex, newItems) {
767
+ $index.set(newIndex);
768
+ $first.set(newIndex === 0);
769
+ $last.set(newIndex === newItems.length - 1);
770
+ $even.set(newIndex % 2 === 0);
771
+ $odd.set(newIndex % 2 !== 0);
772
+ }
666
773
  };
774
+ Object.entries(retVal.vars).forEach(([key, value]) => context.addIdentifier(key, value));
775
+ return retVal;
667
776
  }
668
777
  //#endregion
669
778
  //#region ../packages/core/src/utils/if.util.ts
@@ -683,31 +792,22 @@ function _iterationVariables(items, index, itemName, aliases) {
683
792
  *
684
793
  * @param parentNode - The parent HTML element where the conditional structure is applied.
685
794
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
686
- * @param unwatchFns - Shared array that collects all active cleanup functions.
687
- * Mutated in place: the created effect and branch functions are added to and
688
- * removed from this array.
689
795
  * @param blocks - Ordered list of conditional branches to evaluate.
690
796
  */
691
797
  function _if(parentNode, parentContext, blocks) {
798
+ const anchor = createAnchor("if", parentNode, parentContext);
692
799
  let state;
693
800
  let fn;
694
801
  switch (blocks.length) {
695
802
  case 1:
696
- fn = (state) => handleIf(parentNode, parentContext, blocks[0], state);
803
+ fn = (state) => handleIf(parentNode, parentContext, blocks[0], state, anchor);
697
804
  break;
698
805
  case 2:
699
- fn = (state) => handleIfElse(parentNode, parentContext, blocks[0], blocks[1], state);
806
+ fn = (state) => handleIfElse(parentNode, parentContext, blocks[0], blocks[1], state, anchor);
700
807
  break;
701
- default: fn = (state) => handleIfElseIf(parentNode, parentContext, blocks, state);
808
+ default: fn = (state) => handleIfElseIf(parentNode, parentContext, blocks, state, anchor);
702
809
  }
703
- const unlistener = effect(() => {
704
- if (state) {
705
- state.context.unlisten();
706
- parentContext.removeChild(state.context);
707
- }
708
- state = fn(state);
709
- if (state) parentContext.addChild(state.context);
710
- });
810
+ const unlistener = effect(() => state = fn(state));
711
811
  parentContext.listen(unlistener);
712
812
  }
713
813
  /**
@@ -724,13 +824,9 @@ function _if(parentNode, parentContext, blocks) {
724
824
  * @returns The new state and new cleanup functions if the active branch changed,
725
825
  * otherwise `undefined`.
726
826
  */
727
- function handleIf(parentNode, parentContext, ifBlock, state) {
728
- if (ifBlock.condition()) return checkAndUpdateState(parentNode, parentContext, state, 0, ifBlock.block);
729
- else if (state) {
730
- const context = state.context;
731
- context.unlisten();
732
- parentContext.removeChild(context);
733
- }
827
+ function handleIf(parentNode, parentContext, ifBlock, state, anchor) {
828
+ if (ifBlock.condition()) return checkAndUpdateState(parentNode, parentContext, state, 0, ifBlock.block, anchor);
829
+ teardown(parentContext, state);
734
830
  }
735
831
  /**
736
832
  * Handles the `if` / `else` case (exactly two branches).
@@ -746,8 +842,8 @@ function handleIf(parentNode, parentContext, ifBlock, state) {
746
842
  * @returns The new state and new cleanup functions if the active branch changed,
747
843
  * otherwise `undefined`.
748
844
  */
749
- function handleIfElse(parentNode, parentContext, ifBlock, elseBlock, state) {
750
- return ifBlock.condition() ? checkAndUpdateState(parentNode, parentContext, state, 0, ifBlock.block) : checkAndUpdateState(parentNode, parentContext, state, 1, elseBlock.block);
845
+ function handleIfElse(parentNode, parentContext, ifBlock, elseBlock, state, anchor) {
846
+ return ifBlock.condition() ? checkAndUpdateState(parentNode, parentContext, state, 0, ifBlock.block, anchor) : checkAndUpdateState(parentNode, parentContext, state, 1, elseBlock.block, anchor);
751
847
  }
752
848
  /**
753
849
  * Handles the general case of an `if` / `else if` / ... / `else` chain (three or more branches).
@@ -763,10 +859,10 @@ function handleIfElse(parentNode, parentContext, ifBlock, elseBlock, state) {
763
859
  * @returns The new state and new cleanup functions if the active branch changed,
764
860
  * otherwise `undefined`. Also returns `undefined` if no branch matches.
765
861
  */
766
- function handleIfElseIf(parentNode, parentContext, blocks, state) {
862
+ function handleIfElseIf(parentNode, parentContext, blocks, state, anchor) {
767
863
  for (let i = 0; i < blocks.length; i++) {
768
864
  const { condition, block } = blocks[i];
769
- if (!condition || condition()) return checkAndUpdateState(parentNode, parentContext, state, i, block);
865
+ if (!condition || condition()) return checkAndUpdateState(parentNode, parentContext, state, i, block, anchor);
770
866
  }
771
867
  }
772
868
  /**
@@ -789,13 +885,34 @@ function handleIfElseIf(parentNode, parentContext, blocks, state) {
789
885
  * @returns The updated state if the active branch changed, or `undefined` if the branch
790
886
  * is unchanged.
791
887
  */
792
- function checkAndUpdateState(parentNode, parentContext, state, newState, conditionalBlockFn) {
793
- if (state?.activeBranch !== newState) {
794
- state?.context.unlisten();
795
- return {
796
- activeBranch: newState,
797
- context: untracked(() => conditionalBlockFn(parentNode, parentContext))
798
- };
888
+ function checkAndUpdateState(parentNode, parentContext, state, newState, conditionalBlockFn, anchor) {
889
+ if (state?.activeBranch === newState) return state;
890
+ if (state) {
891
+ state.context.unlisten();
892
+ parentContext.removeChild(state.context);
893
+ }
894
+ const context = untracked(() => conditionalBlockFn(parentNode, parentContext, anchor));
895
+ parentContext.addChild(context);
896
+ return {
897
+ activeBranch: newState,
898
+ context
899
+ };
900
+ }
901
+ /**
902
+ * Handles the case where no branch condition matched: tears down the previously
903
+ * active branch, if one existed, by unlistening its context (running cleanup
904
+ * functions and destroying its subtree) and detaching it from the parent context.
905
+ * A no-op if no branch was previously active.
906
+ *
907
+ * @param parentContext - The parent Context from which the previous branch's
908
+ * context should be detached.
909
+ * @param state - The current state (active branch index and its context), or
910
+ * `undefined` if no branch is currently active.
911
+ */
912
+ function teardown(parentContext, state) {
913
+ if (state) {
914
+ state.context.unlisten();
915
+ parentContext.removeChild(state.context);
799
916
  }
800
917
  }
801
918
  //#endregion
@@ -817,9 +934,9 @@ function checkAndUpdateState(parentNode, parentContext, state, newState, conditi
817
934
  * @param events - List of event listener descriptors to attach to the element.
818
935
  * @returns The newly created HTML element.
819
936
  */
820
- function _renderElement(parentNode, context, tagName, attributes, events) {
937
+ function _renderElement(parentNode, context, anchor, tagName, attributes, events) {
821
938
  const element = document.createElement(tagName);
822
- mountNode(element, parentNode, context);
939
+ mountNode(element, parentNode, context, anchor);
823
940
  attributes.forEach(({ name, value, literal }) => {
824
941
  literal ? element.setAttribute(name, String(value())) : context.listen(effect(() => element.setAttribute(name, String(value()))));
825
942
  });
@@ -880,8 +997,6 @@ function _renderLiteralText(parentNode, context, text) {
880
997
  *
881
998
  * @param parentNode - The parent HTML element where the conditional structure is applied.
882
999
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
883
- * @param unwatchFns - Shared array that collects all active cleanup functions.
884
- * Mutated in place by the underlying {@link _if} call.
885
1000
  * @param expression - Function that evaluates the switch expression. Called
886
1001
  * reactively inside an effect so that signal reads are tracked.
887
1002
  * @param blocks - Ordered list of case branches. Each entry contains:
@@ -897,4 +1012,4 @@ function _switch(parentNode, parentContext, expression, blocks) {
897
1012
  })));
898
1013
  }
899
1014
  //#endregion
900
- export { BaseWebComponent, Context, Event, Property, WebComponent, _for, _if, _iterationVariables, _renderElement, _renderLiteralText, _renderText, _switch, computed, effect, input, mountNode, signal, untracked };
1015
+ export { BaseWebComponent, Context, Event, Property, WebComponent, _for, _if, _iterationVariables, _renderElement, _renderLiteralText, _renderText, _switch, computed, createAnchor, effect, input, mountNode, signal, untracked };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaendar/core",
3
- "version": "0.6.20",
3
+ "version": "0.7.0",
4
4
  "description": "A library containing core utils such as webcomponent base classes and theming support",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@xaendar/signals": "0.6.20",
20
- "@xaendar/types": "0.6.20"
19
+ "@xaendar/signals": "0.7.0",
20
+ "@xaendar/types": "0.7.0"
21
21
  }
22
22
  }