@xaendar/core 0.6.19 → 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,15 +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 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.
277
333
  */
278
- export declare function _for(parentNode: HTMLElement, parentContext: Context, condition: () => unknown[], 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;
279
340
 
280
341
  /**
281
342
  * Creates a reactive conditional structure from a list of branches.
@@ -293,9 +354,6 @@ export declare function _for(parentNode: HTMLElement, parentContext: Context, co
293
354
  *
294
355
  * @param parentNode - The parent HTML element where the conditional structure is applied.
295
356
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
296
- * @param unwatchFns - Shared array that collects all active cleanup functions.
297
- * Mutated in place: the created effect and branch functions are added to and
298
- * removed from this array.
299
357
  * @param blocks - Ordered list of conditional branches to evaluate.
300
358
  */
301
359
  export declare function _if(parentNode: HTMLElement, parentContext: Context, blocks: Block[]): void;
@@ -366,21 +424,47 @@ export declare type InputSignalOptions<ActualValue = unknown, IncomingValue = Ac
366
424
 
367
425
  /**
368
426
  * Builds a record of iteration context variables for a given index in the loop.
369
- * 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.
370
434
  *
371
435
  * @param items - The full array being iterated.
372
436
  * @param index - The current iteration index.
373
- * @param itemName - The identifier to reference the i-th item during iteration
374
- * @param aliases - Aliases for implicit variables defined in the `{@for} loop`
375
- * @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.
376
440
  */
377
- 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: {
378
442
  $index: string;
379
443
  $first: string;
380
444
  $last: string;
381
445
  $even: string;
382
446
  $odd: string;
383
- }): Record<string, unknown>;
447
+ }): IterationVariablesHandle;
448
+
449
+ declare type IterationVariablesHandle = {
450
+ vars: Record<string, unknown>;
451
+ update: (newIndex: number, items: unknown[]) => void;
452
+ };
453
+
454
+ /**
455
+ * Mounts a node into the DOM and binds it to the context lifecycle.
456
+ *
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.
460
+ *
461
+ * @param node - The node to mount.
462
+ * @param parentNode - The parent HTML element to insert the node into.
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`.
466
+ */
467
+ export declare function mountNode(node: Node, parentNode: HTMLElement, context: Context, referenceNode?: Comment | null): void;
384
468
 
385
469
  /**
386
470
  * Represents the output type returned by an `@Event` decorator in a web component.
@@ -471,7 +555,7 @@ declare type PropertyDecoratorOptionsWithRequired<ActualValue = unknown, Incomin
471
555
  * @param events - List of event listener descriptors to attach to the element.
472
556
  * @returns The newly created HTML element.
473
557
  */
474
- 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;
475
559
 
476
560
  /**
477
561
  * Describes a single HTML attribute to be applied to a rendered DOM element.
@@ -592,8 +676,6 @@ export { Signal_2 as Signal }
592
676
  *
593
677
  * @param parentNode - The parent HTML element where the conditional structure is applied.
594
678
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
595
- * @param unwatchFns - Shared array that collects all active cleanup functions.
596
- * Mutated in place by the underlying {@link _if} call.
597
679
  * @param expression - Function that evaluates the switch expression. Called
598
680
  * reactively inside an effect so that signal reads are tracked.
599
681
  * @param blocks - Ordered list of case branches. Each entry contains:
@@ -604,9 +686,16 @@ export { Signal_2 as Signal }
604
686
  */
605
687
  export declare function _switch(parentNode: HTMLElement, parentContext: Context, expression: NoArgsFunction<unknown>, blocks: Array<{
606
688
  condition: unknown[] | null;
607
- block: Function_2<[HTMLElement, Context], Context>;
689
+ block: Function_2<[HTMLElement, Context, Node | null], Context>;
608
690
  }>): void;
609
691
 
692
+ /**
693
+ * Executes a function without tracking any dependencies.
694
+ * @param fn - The function to execute without tracking.
695
+ * @returns The result of the function execution.
696
+ */
697
+ export declare const untracked: typeof Signal.subtle.untrack;
698
+
610
699
  /**
611
700
  * Decorator that registers a class as a custom web component.
612
701
  *
@@ -486,6 +486,14 @@ function signal(value, options) {
486
486
  return getter;
487
487
  }
488
488
  //#endregion
489
+ //#region ../packages/core/src/signals/untracked.ts
490
+ /**
491
+ * Executes a function without tracking any dependencies.
492
+ * @param fn - The function to execute without tracking.
493
+ * @returns The result of the function execution.
494
+ */
495
+ var untracked = Signal.subtle.untrack;
496
+ //#endregion
489
497
  //#region ../packages/core/src/utils/context.util.ts
490
498
  /**
491
499
  * Tracks identifier scope during run time template function execution
@@ -499,8 +507,17 @@ var Context = class {
499
507
  * Child contexts representing nested scopes (e.g. `@for` loop iterations, `@if` branches).
500
508
  */
501
509
  _children = new Array();
502
- /**
503
- * 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.
504
521
  */
505
522
  _nodes = new Array();
506
523
  /**
@@ -513,14 +530,35 @@ var Context = class {
513
530
  * Creates a new scope context.
514
531
  *
515
532
  * @param _root - Web component reference used to resolve property and method bindings.
516
- * @param _parent - Optional parent context representing the enclosing scope.
517
- * @param _identifiers - Named identifier bindings declared in this scope.
518
533
  */
519
534
  constructor(_root, _parent) {
520
535
  this._root = _root;
521
536
  this._parent = _parent;
522
537
  }
523
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
+ /**
524
562
  * Returns the event handler method bound to the root component instance.
525
563
  *
526
564
  * @param handler - The name of the method on the root component to retrieve.
@@ -543,10 +581,10 @@ var Context = class {
543
581
  * @param context - The child context to remove.
544
582
  */
545
583
  removeChild(context) {
546
- this._children = this._children.filter((child) => child === context);
584
+ this._children = this._children.filter((child) => child !== context);
547
585
  }
548
586
  /**
549
- * Registers a DOM node as owned by this context.
587
+ * Registers a DOM node as directly owned by this context.
550
588
  *
551
589
  * @param node - The DOM node to track.
552
590
  */
@@ -559,7 +597,16 @@ var Context = class {
559
597
  * @param nodeToRemove - The DOM node to untrack.
560
598
  */
561
599
  removeNode(nodeToRemove) {
562
- 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;
563
610
  }
564
611
  /**
565
612
  * Registers one or more cleanup functions to be called when this context is destroyed.
@@ -583,59 +630,149 @@ var Context = class {
583
630
  this._unwatchFns.forEach((fn) => fn());
584
631
  this._children.forEach((child) => child.unlisten());
585
632
  this._unwatchFns = [];
633
+ this._children = [];
586
634
  this._nodes = [];
635
+ this._variables.clear();
587
636
  }
588
637
  };
638
+ /**
639
+ * Mounts a node into the DOM and binds it to the context lifecycle.
640
+ *
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.
644
+ *
645
+ * @param node - The node to mount.
646
+ * @param parentNode - The parent HTML element to insert the node into.
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`.
650
+ */
651
+ function mountNode(node, parentNode, context, referenceNode = null) {
652
+ context.addNode(node);
653
+ parentNode.insertBefore(node, referenceNode);
654
+ context.listen(() => {
655
+ context.removeNode(node);
656
+ if (node.parentNode === parentNode) parentNode.removeChild(node);
657
+ });
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
+ }
589
681
  //#endregion
590
682
  //#region ../packages/core/src/utils/for.util.ts
591
683
  /**
592
- * Reactively iterates over a list of items, re-running the loop whenever the tracked condition changes.
593
- * Previous iteration side-effects are cleaned up before each re-evaluation.
594
- *
595
- * @param parentNode - The parent HTML element where the conditional structure is applied.
596
- * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
597
- * @param condition - A reactive function that returns the array of items to iterate over.
598
- * @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.
599
698
  */
600
- function _for(parentNode, parentContext, condition, forFn) {
601
- let contexts = new Array();
699
+ function _for(parentNode, parentContext, condition, trackExpression, forFn) {
700
+ const anchor = createAnchor("for", parentNode, parentContext);
701
+ let entries = /* @__PURE__ */ new Map();
602
702
  const unlistener = effect(() => {
603
- contexts.forEach((context) => {
604
- context.unlisten();
605
- parentContext.removeChild(context);
606
- });
607
- contexts = [];
608
703
  const items = condition();
609
- Signal.subtle.untrack(() => {
610
- for (let i = 0; i < items.length; i++) {
611
- const context = forFn(parentNode, parentContext, items, i);
612
- contexts.push(context);
613
- parentContext.addChild(context);
704
+ const newKeys = items.map((item) => trackExpression(item));
705
+ const newKeySet = new Set(newKeys);
706
+ untracked(() => {
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);
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;
614
729
  }
730
+ entries = newEntries;
615
731
  });
616
732
  });
617
733
  parentContext.listen(unlistener);
618
734
  }
619
735
  /**
620
736
  * Builds a record of iteration context variables for a given index in the loop.
621
- * 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.
622
744
  *
623
745
  * @param items - The full array being iterated.
624
746
  * @param index - The current iteration index.
625
- * @param itemName - The identifier to reference the i-th item during iteration
626
- * @param aliases - Aliases for implicit variables defined in the `{@for} loop`
627
- * @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.
628
750
  */
629
- function _iterationVariables(items, index, itemName, aliases) {
630
- const even = index % 2 === 0;
631
- return {
632
- [itemName]: items[index],
633
- [aliases.$index]: index,
634
- [aliases.$first]: index === 0,
635
- [aliases.$last]: index === items.length - 1,
636
- [aliases.$even]: even,
637
- [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
+ }
638
773
  };
774
+ Object.entries(retVal.vars).forEach(([key, value]) => context.addIdentifier(key, value));
775
+ return retVal;
639
776
  }
640
777
  //#endregion
641
778
  //#region ../packages/core/src/utils/if.util.ts
@@ -655,31 +792,22 @@ function _iterationVariables(items, index, itemName, aliases) {
655
792
  *
656
793
  * @param parentNode - The parent HTML element where the conditional structure is applied.
657
794
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
658
- * @param unwatchFns - Shared array that collects all active cleanup functions.
659
- * Mutated in place: the created effect and branch functions are added to and
660
- * removed from this array.
661
795
  * @param blocks - Ordered list of conditional branches to evaluate.
662
796
  */
663
797
  function _if(parentNode, parentContext, blocks) {
798
+ const anchor = createAnchor("if", parentNode, parentContext);
664
799
  let state;
665
800
  let fn;
666
801
  switch (blocks.length) {
667
802
  case 1:
668
- fn = (state) => handleIf(parentNode, parentContext, blocks[0], state);
803
+ fn = (state) => handleIf(parentNode, parentContext, blocks[0], state, anchor);
669
804
  break;
670
805
  case 2:
671
- fn = (state) => handleIfElse(parentNode, parentContext, blocks[0], blocks[1], state);
806
+ fn = (state) => handleIfElse(parentNode, parentContext, blocks[0], blocks[1], state, anchor);
672
807
  break;
673
- default: fn = (state) => handleIfElseIf(parentNode, parentContext, blocks, state);
808
+ default: fn = (state) => handleIfElseIf(parentNode, parentContext, blocks, state, anchor);
674
809
  }
675
- const unlistener = effect(() => {
676
- if (state) {
677
- state.context.unlisten();
678
- parentContext.removeChild(state.context);
679
- }
680
- state = fn(state);
681
- if (state) parentContext.addChild(state.context);
682
- });
810
+ const unlistener = effect(() => state = fn(state));
683
811
  parentContext.listen(unlistener);
684
812
  }
685
813
  /**
@@ -696,13 +824,9 @@ function _if(parentNode, parentContext, blocks) {
696
824
  * @returns The new state and new cleanup functions if the active branch changed,
697
825
  * otherwise `undefined`.
698
826
  */
699
- function handleIf(parentNode, parentContext, ifBlock, state) {
700
- if (ifBlock.condition()) return checkAndUpdateState(parentNode, parentContext, state, 0, ifBlock.block);
701
- else if (state) {
702
- const context = state.context;
703
- context.unlisten();
704
- parentContext.removeChild(context);
705
- }
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);
706
830
  }
707
831
  /**
708
832
  * Handles the `if` / `else` case (exactly two branches).
@@ -718,8 +842,8 @@ function handleIf(parentNode, parentContext, ifBlock, state) {
718
842
  * @returns The new state and new cleanup functions if the active branch changed,
719
843
  * otherwise `undefined`.
720
844
  */
721
- function handleIfElse(parentNode, parentContext, ifBlock, elseBlock, state) {
722
- 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);
723
847
  }
724
848
  /**
725
849
  * Handles the general case of an `if` / `else if` / ... / `else` chain (three or more branches).
@@ -735,10 +859,10 @@ function handleIfElse(parentNode, parentContext, ifBlock, elseBlock, state) {
735
859
  * @returns The new state and new cleanup functions if the active branch changed,
736
860
  * otherwise `undefined`. Also returns `undefined` if no branch matches.
737
861
  */
738
- function handleIfElseIf(parentNode, parentContext, blocks, state) {
862
+ function handleIfElseIf(parentNode, parentContext, blocks, state, anchor) {
739
863
  for (let i = 0; i < blocks.length; i++) {
740
864
  const { condition, block } = blocks[i];
741
- if (!condition || condition()) return checkAndUpdateState(parentNode, parentContext, state, i, block);
865
+ if (!condition || condition()) return checkAndUpdateState(parentNode, parentContext, state, i, block, anchor);
742
866
  }
743
867
  }
744
868
  /**
@@ -761,13 +885,34 @@ function handleIfElseIf(parentNode, parentContext, blocks, state) {
761
885
  * @returns The updated state if the active branch changed, or `undefined` if the branch
762
886
  * is unchanged.
763
887
  */
764
- function checkAndUpdateState(parentNode, parentContext, state, newState, conditionalBlockFn) {
765
- if (state?.activeBranch !== newState) {
766
- state?.context.unlisten();
767
- return {
768
- activeBranch: newState,
769
- context: Signal.subtle.untrack(() => conditionalBlockFn(parentNode, parentContext))
770
- };
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);
771
916
  }
772
917
  }
773
918
  //#endregion
@@ -789,13 +934,9 @@ function checkAndUpdateState(parentNode, parentContext, state, newState, conditi
789
934
  * @param events - List of event listener descriptors to attach to the element.
790
935
  * @returns The newly created HTML element.
791
936
  */
792
- function _renderElement(parentNode, context, tagName, attributes, events) {
937
+ function _renderElement(parentNode, context, anchor, tagName, attributes, events) {
793
938
  const element = document.createElement(tagName);
794
- parentNode.appendChild(element);
795
- context.listen(() => {
796
- context.removeNode(element);
797
- parentNode.removeChild(element);
798
- });
939
+ mountNode(element, parentNode, context, anchor);
799
940
  attributes.forEach(({ name, value, literal }) => {
800
941
  literal ? element.setAttribute(name, String(value())) : context.listen(effect(() => element.setAttribute(name, String(value()))));
801
942
  });
@@ -822,11 +963,7 @@ function _renderElement(parentNode, context, tagName, attributes, events) {
822
963
  */
823
964
  function _renderText(parentNode, context, textFn) {
824
965
  const node = document.createTextNode(textFn());
825
- parentNode.appendChild(node);
826
- context.listen(() => {
827
- context.removeNode(node);
828
- parentNode.removeChild(node);
829
- });
966
+ mountNode(node, parentNode, context);
830
967
  context.listen(effect(() => node.textContent = textFn()));
831
968
  }
832
969
  /**
@@ -841,9 +978,7 @@ function _renderText(parentNode, context, textFn) {
841
978
  * @param text - The literal string to render as text content.
842
979
  */
843
980
  function _renderLiteralText(parentNode, context, text) {
844
- const node = document.createTextNode(text);
845
- parentNode.appendChild(node);
846
- context.listen(() => parentNode.removeChild(node));
981
+ mountNode(document.createTextNode(text), parentNode, context);
847
982
  }
848
983
  //#endregion
849
984
  //#region ../packages/core/src/utils/switch.util.ts
@@ -862,8 +997,6 @@ function _renderLiteralText(parentNode, context, text) {
862
997
  *
863
998
  * @param parentNode - The parent HTML element where the conditional structure is applied.
864
999
  * @param parentContext - The parent Context object containing all the variables definition from the Parent Closure
865
- * @param unwatchFns - Shared array that collects all active cleanup functions.
866
- * Mutated in place by the underlying {@link _if} call.
867
1000
  * @param expression - Function that evaluates the switch expression. Called
868
1001
  * reactively inside an effect so that signal reads are tracked.
869
1002
  * @param blocks - Ordered list of case branches. Each entry contains:
@@ -879,4 +1012,4 @@ function _switch(parentNode, parentContext, expression, blocks) {
879
1012
  })));
880
1013
  }
881
1014
  //#endregion
882
- export { BaseWebComponent, Context, Event, Property, WebComponent, _for, _if, _iterationVariables, _renderElement, _renderLiteralText, _renderText, _switch, computed, effect, input, signal };
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.19",
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.19",
20
- "@xaendar/types": "0.6.19"
19
+ "@xaendar/signals": "0.7.0",
20
+ "@xaendar/types": "0.7.0"
21
21
  }
22
22
  }