@mmstack/primitives 22.1.2 → 22.2.1

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
@@ -418,6 +418,8 @@ export class UserProfile {
418
418
 
419
419
  This is also the pattern for coordinating resources registered _above_ a boundary (e.g. an app-builder page whose connectors register at a higher injector): the outer `provideTransitionScope()` is the shared scope, and any number of `<mm-unscoped-suspense>` boundaries observe it.
420
420
 
421
+ **Forwarding scope (advanced).** `provideForwardingTransitionScope()` provides a scope that can be **re-pointed at a different target at runtime** via `setTarget(scope | null)` — reads follow the current target, while `add`/`remove` pin to the target a resource was registered under (so re-pointing never strands a registration). It's the building block for a coordinator that hosts several independent sub-scopes and switches which one it observes — e.g. a router outlet that, per navigation, points at the incoming route's own scope (read it from any injector with `getTransitionScope(injector)`). Most apps reach for `provideTransitionScope()`; this is for that one extra level of control.
422
+
421
423
  ### `injectStartTransition`
422
424
 
423
425
  The analog of React's `useTransition`. `startTransition(fn)` runs your state mutations (which commit immediately); any resource that reloads as a result **holds its value and reveals together once everything settles** — so a multi-resource update lands as one consistent frame instead of a torn mix of new and stale. The returned handle gives you a unified `pending` signal and a `done` promise for imperative coordination (disable a button, await completion).
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, InjectionToken, TemplateRef, ViewContainerRef, input, computed, Directive, signal, PLATFORM_ID, runInInjectionContext, afterNextRender, Component, isWritableSignal as isWritableSignal$2, isSignal, ElementRef, Injectable } from '@angular/core';
2
+ import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, InjectionToken, TemplateRef, ViewContainerRef, PLATFORM_ID, input, computed, Directive, signal, runInInjectionContext, afterNextRender, Component, isWritableSignal as isWritableSignal$2, isSignal, ElementRef, Injectable } from '@angular/core';
3
3
  import { isPlatformServer } from '@angular/common';
4
4
  import { SIGNAL } from '@angular/core/primitives/signals';
5
5
 
@@ -80,7 +80,7 @@ function popFrame() {
80
80
  * ]);
81
81
  *
82
82
  * // The fine-grained mapped list
83
- * const mappedUsers = mapArray(
83
+ * const mappedUsers = indexArray(
84
84
  * users,
85
85
  * (userSignal, index) => {
86
86
  * // 1. Create a fine-grained SIDE EFFECT for *this item*
@@ -101,7 +101,7 @@ function popFrame() {
101
101
  * };
102
102
  * },
103
103
  * {
104
- * // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
104
+ * // 3. Tell indexArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
105
105
  * onDestroy: (mappedItem) => {
106
106
  * mappedItem.destroyEffect();
107
107
  * }
@@ -246,6 +246,7 @@ class MmActivity {
246
246
  tpl = inject(TemplateRef);
247
247
  vcr = inject(ViewContainerRef);
248
248
  parent = inject(Injector);
249
+ onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
249
250
  /** When false, keep the content mounted but hidden + CD-detached. */
250
251
  visible = input.required({ ...(ngDevMode ? { debugName: "visible" } : /* istanbul ignore next */ {}), alias: 'mmActivity' });
251
252
  /** Paused == not visible — handed to the kept subtree as PAUSED_CONTEXT. */
@@ -269,6 +270,8 @@ class MmActivity {
269
270
  }),
270
271
  });
271
272
  }
273
+ if (this.onServer)
274
+ return;
272
275
  for (const node of this.view.rootNodes) {
273
276
  // covers HTML and SVG roots; text/comment roots can't be styled — their CD is still
274
277
  // detached, but prefer an element root for true visual hiding
@@ -280,10 +283,10 @@ class MmActivity {
280
283
  else
281
284
  this.view.detach();
282
285
  }
283
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MmActivity, deps: [], target: i0.ɵɵFactoryTarget.Directive });
284
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.0", type: MmActivity, isStandalone: true, selector: "[mmActivity]", inputs: { visible: { classPropertyName: "visible", publicName: "mmActivity", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
286
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmActivity, deps: [], target: i0.ɵɵFactoryTarget.Directive });
287
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: MmActivity, isStandalone: true, selector: "[mmActivity]", inputs: { visible: { classPropertyName: "visible", publicName: "mmActivity", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
285
288
  }
286
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MmActivity, decorators: [{
289
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmActivity, decorators: [{
287
290
  type: Directive,
288
291
  args: [{
289
292
  selector: '[mmActivity]',
@@ -565,6 +568,48 @@ function injectTransitionScope() {
565
568
  }
566
569
  return scope;
567
570
  }
571
+ function createForwardingScope() {
572
+ const own = createTransitionScope();
573
+ const target = signal(null, /* @ts-ignore */
574
+ ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
575
+ const eff = () => target() ?? own;
576
+ const owners = new Map();
577
+ return {
578
+ setTarget: (t) => target.set(t),
579
+ resources: computed(() => eff().resources()),
580
+ pending: computed(() => eff().pending()),
581
+ suspended: (type) => eff().suspended(type),
582
+ add: (ref, opt) => {
583
+ const t = untracked(target) ?? own;
584
+ owners.set(ref, t);
585
+ t.add(ref, opt);
586
+ },
587
+ remove: (ref) => {
588
+ const t = owners.get(ref) ?? untracked(target) ?? own;
589
+ t.remove(ref);
590
+ owners.delete(ref);
591
+ },
592
+ commit: (value) => linkedSignal({
593
+ source: () => ({ v: value(), settled: !eff().pending() }),
594
+ computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
595
+ }),
596
+ holding: computed(() => eff().holding()),
597
+ beginHold: () => (untracked(target) ?? own).beginHold(),
598
+ endHold: () => (untracked(target) ?? own).endHold(),
599
+ hold: (value) => linkedSignal({
600
+ source: () => ({ v: value(), held: eff().holding() }),
601
+ computation: (curr, prev) => prev !== undefined && curr.held ? prev.value : curr.v,
602
+ }),
603
+ };
604
+ }
605
+ /** Provide a forwarding transition scope at a boundary (used by the transition outlet). */
606
+ function provideForwardingTransitionScope() {
607
+ return { provide: TRANSITION_SCOPE, useFactory: createForwardingScope };
608
+ }
609
+ /** Read the transition scope reachable from `injector`, or null if none is provided there. */
610
+ function getTransitionScope(injector) {
611
+ return injector.get(TRANSITION_SCOPE, null);
612
+ }
568
613
  /**
569
614
  * Returns a register function bound to the nearest transition scope: it adds a resource
570
615
  * to the scope and removes it when the caller's injection context is destroyed. Pass any
@@ -602,6 +647,7 @@ function registerResource(res, opt) {
602
647
  function injectStartTransition() {
603
648
  const scope = injectTransitionScope();
604
649
  const injector = inject(Injector);
650
+ const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
605
651
  return (fn) => {
606
652
  untracked(fn);
607
653
  let sawPending = false;
@@ -616,6 +662,13 @@ function injectStartTransition() {
616
662
  resolve();
617
663
  }
618
664
  }, { ...(ngDevMode ? { debugName: "watcher" } : /* istanbul ignore next */ {}), injector });
665
+ if (onServer) {
666
+ if (!untracked(scope.pending)) {
667
+ watcher.destroy();
668
+ resolve();
669
+ }
670
+ return;
671
+ }
619
672
  // no-async fallback: once the reactive system has processed the writes (afterNextRender),
620
673
  // if nothing ever went in flight, the transition is already complete.
621
674
  afterNextRender(() => {
@@ -652,10 +705,10 @@ class SuspenseBoundaryBase {
652
705
  pending = this.scope.pending;
653
706
  suspended = computed(() => this.scope.suspended(this.type()), /* @ts-ignore */
654
707
  ...(ngDevMode ? [{ debugName: "suspended" }] : /* istanbul ignore next */ []));
655
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SuspenseBoundaryBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
656
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.0", type: SuspenseBoundaryBase, isStandalone: true, inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
708
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundaryBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
709
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: SuspenseBoundaryBase, isStandalone: true, inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
657
710
  }
658
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SuspenseBoundaryBase, decorators: [{
711
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundaryBase, decorators: [{
659
712
  type: Directive
660
713
  }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } });
661
714
  const SUSPENSE_TEMPLATE = `
@@ -683,10 +736,10 @@ const SUSPENSE_HOST = {
683
736
  * `provideTransitionScope()`. The common case.
684
737
  */
685
738
  class SuspenseBoundary extends SuspenseBoundaryBase {
686
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
687
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: SuspenseBoundary, isStandalone: true, selector: "mm-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, providers: [provideTransitionScope()], usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
739
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
740
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SuspenseBoundary, isStandalone: true, selector: "mm-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, providers: [provideTransitionScope()], usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
688
741
  }
689
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SuspenseBoundary, decorators: [{
742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundary, decorators: [{
690
743
  type: Component,
691
744
  args: [{ selector: 'mm-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, providers: [provideTransitionScope()], styles: [":host{display:contents}\n"] }]
692
745
  }] });
@@ -698,10 +751,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImpor
698
751
  * ancestor.
699
752
  */
700
753
  class UnscopedSuspenseBoundary extends SuspenseBoundaryBase {
701
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: UnscopedSuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
702
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: UnscopedSuspenseBoundary, isStandalone: true, selector: "mm-unscoped-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
754
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: UnscopedSuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
755
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: UnscopedSuspenseBoundary, isStandalone: true, selector: "mm-unscoped-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
703
756
  }
704
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: UnscopedSuspenseBoundary, decorators: [{
757
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: UnscopedSuspenseBoundary, decorators: [{
705
758
  type: Component,
706
759
  args: [{ selector: 'mm-unscoped-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, styles: [":host{display:contents}\n"] }]
707
760
  }] });
@@ -758,6 +811,7 @@ function runInTransaction(txn, fn) {
758
811
  function injectStartTransaction() {
759
812
  const scope = injectTransitionScope();
760
813
  const injector = inject(Injector);
814
+ const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
761
815
  return (fn) => {
762
816
  const txn = createTransaction();
763
817
  // Hold BEFORE the writes, so the display freezes at pre-transaction values.
@@ -799,11 +853,17 @@ function injectStartTransaction() {
799
853
  if (sawPending && !p)
800
854
  finish(false);
801
855
  }, { injector });
802
- // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
803
- afterNextRender(() => {
804
- if (!sawPending && !untracked(scope.pending))
856
+ if (onServer) {
857
+ if (!untracked(scope.pending))
805
858
  finish(false);
806
- }, { injector });
859
+ }
860
+ else {
861
+ // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
862
+ afterNextRender(() => {
863
+ if (!sawPending && !untracked(scope.pending))
864
+ finish(false);
865
+ }, { injector });
866
+ }
807
867
  return {
808
868
  pending: scope.pending,
809
869
  done,
@@ -3970,10 +4030,10 @@ class MessageBus {
3970
4030
  this.channel.close();
3971
4031
  this.listeners.clear();
3972
4032
  }
3973
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3974
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MessageBus, providedIn: 'root' });
4033
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4034
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, providedIn: 'root' });
3975
4035
  }
3976
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: MessageBus, decorators: [{
4036
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, decorators: [{
3977
4037
  type: Injectable,
3978
4038
  args: [{
3979
4039
  providedIn: 'root',
@@ -4278,5 +4338,5 @@ function withHistory(sourceOrValue, opt) {
4278
4338
  * Generated bundle index. Do not edit.
4279
4339
  */
4280
4340
 
4281
- export { MmActivity, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, forkStore, geolocation, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
4341
+ export { MmActivity, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
4282
4342
  //# sourceMappingURL=mmstack-primitives.mjs.map