@mmstack/primitives 20.6.2 → 20.7.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, 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, 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
  * }
@@ -250,6 +250,7 @@ class MmActivity {
250
250
  tpl = inject(TemplateRef);
251
251
  vcr = inject(ViewContainerRef);
252
252
  parent = inject(Injector);
253
+ onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
253
254
  /** When false, keep the content mounted but hidden + CD-detached. */
254
255
  visible = input.required(...(ngDevMode ? [{ debugName: "visible", alias: 'mmActivity' }] : [{ alias: 'mmActivity' }]));
255
256
  /** Paused == not visible — handed to the kept subtree as PAUSED_CONTEXT. */
@@ -272,6 +273,8 @@ class MmActivity {
272
273
  }),
273
274
  });
274
275
  }
276
+ if (this.onServer)
277
+ return;
275
278
  for (const node of this.view.rootNodes) {
276
279
  // covers HTML and SVG roots; text/comment roots can't be styled — their CD is still
277
280
  // detached, but prefer an element root for true visual hiding
@@ -573,6 +576,47 @@ function injectTransitionScope() {
573
576
  }
574
577
  return scope;
575
578
  }
579
+ function createForwardingScope() {
580
+ const own = createTransitionScope();
581
+ const target = signal(null, ...(ngDevMode ? [{ debugName: "target" }] : []));
582
+ const eff = () => target() ?? own;
583
+ const owners = new Map();
584
+ return {
585
+ setTarget: (t) => target.set(t),
586
+ resources: computed(() => eff().resources()),
587
+ pending: computed(() => eff().pending()),
588
+ suspended: (type) => eff().suspended(type),
589
+ add: (ref, opt) => {
590
+ const t = untracked(target) ?? own;
591
+ owners.set(ref, t);
592
+ t.add(ref, opt);
593
+ },
594
+ remove: (ref) => {
595
+ const t = owners.get(ref) ?? untracked(target) ?? own;
596
+ t.remove(ref);
597
+ owners.delete(ref);
598
+ },
599
+ commit: (value) => linkedSignal({
600
+ source: () => ({ v: value(), settled: !eff().pending() }),
601
+ computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
602
+ }),
603
+ holding: computed(() => eff().holding()),
604
+ beginHold: () => (untracked(target) ?? own).beginHold(),
605
+ endHold: () => (untracked(target) ?? own).endHold(),
606
+ hold: (value) => linkedSignal({
607
+ source: () => ({ v: value(), held: eff().holding() }),
608
+ computation: (curr, prev) => prev !== undefined && curr.held ? prev.value : curr.v,
609
+ }),
610
+ };
611
+ }
612
+ /** Provide a forwarding transition scope at a boundary (used by the transition outlet). */
613
+ function provideForwardingTransitionScope() {
614
+ return { provide: TRANSITION_SCOPE, useFactory: createForwardingScope };
615
+ }
616
+ /** Read the transition scope reachable from `injector`, or null if none is provided there. */
617
+ function getTransitionScope(injector) {
618
+ return injector.get(TRANSITION_SCOPE, null);
619
+ }
576
620
  /**
577
621
  * Returns a register function bound to the nearest transition scope: it adds a resource
578
622
  * to the scope and removes it when the caller's injection context is destroyed. Pass any
@@ -610,6 +654,7 @@ function registerResource(res, opt) {
610
654
  function injectStartTransition() {
611
655
  const scope = injectTransitionScope();
612
656
  const injector = inject(Injector);
657
+ const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
613
658
  return (fn) => {
614
659
  untracked(fn);
615
660
  let sawPending = false;
@@ -624,6 +669,13 @@ function injectStartTransition() {
624
669
  resolve();
625
670
  }
626
671
  }, ...(ngDevMode ? [{ debugName: "watcher", injector }] : [{ injector }]));
672
+ if (onServer) {
673
+ if (!untracked(scope.pending)) {
674
+ watcher.destroy();
675
+ resolve();
676
+ }
677
+ return;
678
+ }
627
679
  // no-async fallback: once the reactive system has processed the writes (afterNextRender),
628
680
  // if nothing ever went in flight, the transition is already complete.
629
681
  afterNextRender(() => {
@@ -764,6 +816,7 @@ function runInTransaction(txn, fn) {
764
816
  function injectStartTransaction() {
765
817
  const scope = injectTransitionScope();
766
818
  const injector = inject(Injector);
819
+ const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
767
820
  return (fn) => {
768
821
  const txn = createTransaction();
769
822
  // Hold BEFORE the writes, so the display freezes at pre-transaction values.
@@ -805,11 +858,17 @@ function injectStartTransaction() {
805
858
  if (sawPending && !p)
806
859
  finish(false);
807
860
  }, { injector });
808
- // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
809
- afterNextRender(() => {
810
- if (!sawPending && !untracked(scope.pending))
861
+ if (onServer) {
862
+ if (!untracked(scope.pending))
811
863
  finish(false);
812
- }, { injector });
864
+ }
865
+ else {
866
+ // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
867
+ afterNextRender(() => {
868
+ if (!sawPending && !untracked(scope.pending))
869
+ finish(false);
870
+ }, { injector });
871
+ }
813
872
  return {
814
873
  pending: scope.pending,
815
874
  done,
@@ -4278,5 +4337,5 @@ function withHistory(sourceOrValue, opt) {
4278
4337
  * Generated bundle index. Do not edit.
4279
4338
  */
4280
4339
 
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 };
4340
+ 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
4341
  //# sourceMappingURL=mmstack-primitives.mjs.map