@mmstack/primitives 20.5.6 → 20.5.7
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/fesm2022/mmstack-primitives.mjs +206 -11
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +265 -11
- package/package.json +1 -1
|
@@ -604,7 +604,30 @@ const mapArray = indexArray;
|
|
|
604
604
|
* @param mapFn The mapping function. Receives the item and its index as a Signal.
|
|
605
605
|
* @param options Optional configuration:
|
|
606
606
|
* - `onDestroy`: A callback invoked when a mapped item is removed from the array.
|
|
607
|
+
* - `key`: A custom key extractor for identity matching (e.g. `(item) => item.id`)
|
|
608
|
+
* when item references change but conceptual identity is preserved.
|
|
607
609
|
* @returns A `Signal<U[]>` containing the mapped array.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```ts
|
|
613
|
+
* const users = signal([
|
|
614
|
+
* { id: 1, name: 'Alice' },
|
|
615
|
+
* { id: 2, name: 'Bob' },
|
|
616
|
+
* ]);
|
|
617
|
+
*
|
|
618
|
+
* const rows = keyArray(
|
|
619
|
+
* users,
|
|
620
|
+
* (user, index) => ({
|
|
621
|
+
* label: computed(() => `#${index()} ${user.name}`),
|
|
622
|
+
* id: user.id,
|
|
623
|
+
* }),
|
|
624
|
+
* { key: (u) => u.id },
|
|
625
|
+
* );
|
|
626
|
+
*
|
|
627
|
+
* // Reordering users() rebuilds index signals only — `rows` entries
|
|
628
|
+
* // are matched by id and reused, not re-created.
|
|
629
|
+
* users.set([users()[1], users()[0]]);
|
|
630
|
+
* ```
|
|
608
631
|
*/
|
|
609
632
|
function keyArray(source, mapFn, options = {}) {
|
|
610
633
|
const sourceSignal = isSignal(source) ? source : computed(source);
|
|
@@ -761,15 +784,69 @@ function mapObject(source, mapFn, options = {}) {
|
|
|
761
784
|
}).asReadonly();
|
|
762
785
|
}
|
|
763
786
|
|
|
764
|
-
/**
|
|
787
|
+
/**
|
|
788
|
+
* Synchronous projection of a signal value with optional `CreateSignalOptions`
|
|
789
|
+
* (custom `equal`, `debugName`, etc.). Equivalent to `map` plus the ability to
|
|
790
|
+
* pass signal options through to the underlying `computed()`.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```ts
|
|
794
|
+
* const user = piped({ id: 1, name: 'Alice' });
|
|
795
|
+
* const name = user.pipe(select((u) => u.name));
|
|
796
|
+
* name(); // 'Alice'
|
|
797
|
+
* ```
|
|
798
|
+
*/
|
|
765
799
|
const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
|
|
766
|
-
/**
|
|
800
|
+
/**
|
|
801
|
+
* Combine the piped signal with another `Signal` using a projector. The result
|
|
802
|
+
* recomputes whenever either source changes.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```ts
|
|
806
|
+
* const price = piped(10);
|
|
807
|
+
* const quantity = signal(3);
|
|
808
|
+
* const total = price.pipe(combineWith(quantity, (p, q) => p * q));
|
|
809
|
+
* total(); // 30
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
767
812
|
const combineWith = (other, project, opt) => (src) => computed(() => project(src(), other()), opt);
|
|
768
|
-
/**
|
|
813
|
+
/**
|
|
814
|
+
* Suppress emissions while consecutive values are considered equal. The
|
|
815
|
+
* comparator defaults to `Object.is`; pass a custom one for structural or
|
|
816
|
+
* key-based equality (e.g. compare by `id` only).
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```ts
|
|
820
|
+
* const user = piped({ id: 1, lastSeen: Date.now() });
|
|
821
|
+
* const byId = user.pipe(distinct((a, b) => a.id === b.id));
|
|
822
|
+
* // byId only re-emits when `id` changes, not on every `lastSeen` update
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
769
825
|
const distinct = (equal = Object.is) => (src) => computed(() => src(), { equal });
|
|
770
|
-
/**
|
|
826
|
+
/**
|
|
827
|
+
* Pure synchronous transform from input to output. Equivalent to a `computed()`
|
|
828
|
+
* that reads the source and returns `fn(value)`.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```ts
|
|
832
|
+
* const count = piped(2);
|
|
833
|
+
* const doubled = count.pipe(map((n) => n * 2));
|
|
834
|
+
* doubled(); // 4
|
|
835
|
+
* ```
|
|
836
|
+
*/
|
|
771
837
|
const map = (fn) => (src) => computed(() => fn(src()));
|
|
772
|
-
/**
|
|
838
|
+
/**
|
|
839
|
+
* Keep only values that pass the predicate. The result holds the last passing
|
|
840
|
+
* value across emissions; before any value passes, the result is `undefined` —
|
|
841
|
+
* see {@link filterWith} when you need a non-`undefined` seed.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```ts
|
|
845
|
+
* const event = piped<MouseEvent | null>(null);
|
|
846
|
+
* const clicks = event.pipe(filter((e): e is MouseEvent => e?.type === 'click'));
|
|
847
|
+
* clicks(); // undefined until a click happens, then the last MouseEvent
|
|
848
|
+
* ```
|
|
849
|
+
*/
|
|
773
850
|
const filter = (predicate) => (src) => linkedSignal({
|
|
774
851
|
source: src,
|
|
775
852
|
computation: (next, prev) => {
|
|
@@ -778,7 +855,19 @@ const filter = (predicate) => (src) => linkedSignal({
|
|
|
778
855
|
return prev?.source;
|
|
779
856
|
},
|
|
780
857
|
});
|
|
781
|
-
/**
|
|
858
|
+
/**
|
|
859
|
+
* Run a side effect on every emission without altering the signal value. Wraps
|
|
860
|
+
* Angular's `effect()`, so it must run in an injection context or receive an
|
|
861
|
+
* explicit `injector`. Use for logging / analytics — not for setting other
|
|
862
|
+
* signals (that's what regular `effect()` is for).
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* ```ts
|
|
866
|
+
* const count = piped(0);
|
|
867
|
+
* count.pipe(tap((n) => console.log('count:', n)));
|
|
868
|
+
* count.set(1); // logs 'count: 1'
|
|
869
|
+
* ```
|
|
870
|
+
*/
|
|
782
871
|
const tap = (fn, injector) => (src) => {
|
|
783
872
|
effect(() => fn(src()), {
|
|
784
873
|
injector,
|
|
@@ -786,24 +875,66 @@ const tap = (fn, injector) => (src) => {
|
|
|
786
875
|
return src;
|
|
787
876
|
};
|
|
788
877
|
/**
|
|
789
|
-
* Like {@link filter}, but emits `initial` until a value passes the
|
|
790
|
-
*
|
|
878
|
+
* Like {@link filter}, but emits `initial` until a value first passes the
|
|
879
|
+
* predicate. Eliminates the `T | undefined` return type at the cost of an
|
|
880
|
+
* explicit seed value.
|
|
881
|
+
*
|
|
882
|
+
* @example
|
|
883
|
+
* ```ts
|
|
884
|
+
* const event = piped<MouseEvent | null>(null);
|
|
885
|
+
* const lastClick = event.pipe(filterWith((e) => e?.type === 'click', null));
|
|
886
|
+
* lastClick(); // null until the first click, then the most recent click event
|
|
887
|
+
* ```
|
|
791
888
|
*/
|
|
792
889
|
const filterWith = (predicate, initial) => (src) => linkedSignal({
|
|
793
890
|
source: src,
|
|
794
891
|
computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
|
|
795
892
|
});
|
|
796
|
-
/**
|
|
893
|
+
/**
|
|
894
|
+
* Emit `initial` on the first read, then mirror the source on every subsequent
|
|
895
|
+
* read. Useful for giving a pipeline a sensible seed value before the source
|
|
896
|
+
* is ready (e.g. loading state).
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* ```ts
|
|
900
|
+
* const data = piped<User | null>(null);
|
|
901
|
+
* const view = data.pipe(startWith<User | null, 'loading'>('loading'));
|
|
902
|
+
* view(); // 'loading' on first read, then User | null afterward
|
|
903
|
+
* ```
|
|
904
|
+
*/
|
|
797
905
|
const startWith = (initial) => (src) => linkedSignal({
|
|
798
906
|
source: src,
|
|
799
907
|
computation: (next, prev) => (prev === undefined ? initial : next),
|
|
800
908
|
});
|
|
801
|
-
/**
|
|
909
|
+
/**
|
|
910
|
+
* Emit `[prev, curr]` tuples so consumers can react to transitions instead of
|
|
911
|
+
* raw values. On the first emission `prev` is `undefined`.
|
|
912
|
+
*
|
|
913
|
+
* @example
|
|
914
|
+
* ```ts
|
|
915
|
+
* const count = piped(0);
|
|
916
|
+
* const delta = count.pipe(pairwise(), map(([prev, curr]) => curr - (prev ?? 0)));
|
|
917
|
+
* count.set(5);
|
|
918
|
+
* delta(); // 5
|
|
919
|
+
* ```
|
|
920
|
+
*/
|
|
802
921
|
const pairwise = () => (src) => linkedSignal({
|
|
803
922
|
source: src,
|
|
804
923
|
computation: (next, prev) => [prev?.source, next],
|
|
805
924
|
});
|
|
806
|
-
/**
|
|
925
|
+
/**
|
|
926
|
+
* Reduce-like accumulator that folds each emission into a running result.
|
|
927
|
+
* Behaves like `Array.prototype.reduce` but applied over time, with the
|
|
928
|
+
* accumulator persisted across emissions.
|
|
929
|
+
*
|
|
930
|
+
* @example
|
|
931
|
+
* ```ts
|
|
932
|
+
* const delta = piped(0);
|
|
933
|
+
* const total = delta.pipe(scan((acc, n) => acc + n, 0));
|
|
934
|
+
* delta.set(5); // total() === 5
|
|
935
|
+
* delta.set(3); // total() === 8
|
|
936
|
+
* ```
|
|
937
|
+
*/
|
|
807
938
|
const scan = (reducer, seed) => (src) => linkedSignal({
|
|
808
939
|
source: src,
|
|
809
940
|
computation: (next, prev) => reducer(prev?.value ?? seed, next),
|
|
@@ -972,6 +1103,15 @@ const EVENTS = [
|
|
|
972
1103
|
* Battery Status API. Returns `null` until the underlying `getBattery()`
|
|
973
1104
|
* promise resolves, or permanently when the API is unsupported (Firefox /
|
|
974
1105
|
* Safari at the time of writing). SSR-safe.
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* ```ts
|
|
1109
|
+
* const battery = batteryStatus();
|
|
1110
|
+
* effect(() => {
|
|
1111
|
+
* const b = battery();
|
|
1112
|
+
* if (b) console.log(`${Math.round(b.level * 100)}% • charging: ${b.charging}`);
|
|
1113
|
+
* });
|
|
1114
|
+
* ```
|
|
975
1115
|
*/
|
|
976
1116
|
function batteryStatus(debugName = 'batteryStatus') {
|
|
977
1117
|
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
@@ -1254,6 +1394,15 @@ function unwrap$1(target) {
|
|
|
1254
1394
|
* Defaults `target` to the current `ElementRef` so it can be used inline in a
|
|
1255
1395
|
* component's `class` field. SSR-safe — returns a constant `false` signal on
|
|
1256
1396
|
* the server.
|
|
1397
|
+
*
|
|
1398
|
+
* @example
|
|
1399
|
+
* ```ts
|
|
1400
|
+
* @Component({ ... })
|
|
1401
|
+
* class MenuComponent {
|
|
1402
|
+
* // Defaults to the host element — flips true when focus is inside.
|
|
1403
|
+
* readonly hasFocus = focusWithin();
|
|
1404
|
+
* }
|
|
1405
|
+
* ```
|
|
1257
1406
|
*/
|
|
1258
1407
|
function focusWithin(target = inject(ElementRef)) {
|
|
1259
1408
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
@@ -1356,6 +1505,14 @@ const serverDate$1 = new Date();
|
|
|
1356
1505
|
* activity) resets the timer and flips the signal back to `false`.
|
|
1357
1506
|
*
|
|
1358
1507
|
* SSR-safe — always `false` with a frozen `since` date on the server.
|
|
1508
|
+
*
|
|
1509
|
+
* @example
|
|
1510
|
+
* ```ts
|
|
1511
|
+
* const isAway = idle({ ms: 30_000 });
|
|
1512
|
+
* effect(() => {
|
|
1513
|
+
* if (isAway()) console.log('idle since', isAway.since());
|
|
1514
|
+
* });
|
|
1515
|
+
* ```
|
|
1359
1516
|
*/
|
|
1360
1517
|
function idle(opt) {
|
|
1361
1518
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
@@ -1719,6 +1876,14 @@ const serverDate = new Date();
|
|
|
1719
1876
|
*
|
|
1720
1877
|
* @param debugName Optional debug name for the signal.
|
|
1721
1878
|
* @returns A `NetworkStatusSignal` instance.
|
|
1879
|
+
*
|
|
1880
|
+
* @example
|
|
1881
|
+
* ```ts
|
|
1882
|
+
* const online = networkStatus();
|
|
1883
|
+
* effect(() => {
|
|
1884
|
+
* if (!online()) console.log('offline since', online.since());
|
|
1885
|
+
* });
|
|
1886
|
+
* ```
|
|
1722
1887
|
*/
|
|
1723
1888
|
function networkStatus(debugName = 'networkStatus') {
|
|
1724
1889
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
@@ -1760,6 +1925,15 @@ const SSR_FALLBACK = {
|
|
|
1760
1925
|
*
|
|
1761
1926
|
* SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
|
|
1762
1927
|
* and in environments without `screen.orientation` support.
|
|
1928
|
+
*
|
|
1929
|
+
* @example
|
|
1930
|
+
* ```ts
|
|
1931
|
+
* const screenOrientation = orientation();
|
|
1932
|
+
* effect(() => {
|
|
1933
|
+
* const { type, angle } = screenOrientation();
|
|
1934
|
+
* console.log(`${type} at ${angle}°`);
|
|
1935
|
+
* });
|
|
1936
|
+
* ```
|
|
1763
1937
|
*/
|
|
1764
1938
|
function orientation(debugName = 'orientation') {
|
|
1765
1939
|
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
@@ -2039,6 +2213,27 @@ function sensor(type, options) {
|
|
|
2039
2213
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
2040
2214
|
}
|
|
2041
2215
|
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Bulk sensor factory — creates several sensor signals at once and returns
|
|
2218
|
+
* them keyed by sensor type. Convenient when a single consumer needs to react
|
|
2219
|
+
* to multiple browser signals; for a single sensor prefer {@link sensor}
|
|
2220
|
+
* directly.
|
|
2221
|
+
*
|
|
2222
|
+
* @typeParam TType The union of sensor keys being requested.
|
|
2223
|
+
* @param track Array of sensor type keys to create.
|
|
2224
|
+
* @param opt Optional per-sensor options keyed by sensor type.
|
|
2225
|
+
* @returns A record `{ [key]: <SensorReturnType> }` for each requested key.
|
|
2226
|
+
*
|
|
2227
|
+
* @example
|
|
2228
|
+
* ```ts
|
|
2229
|
+
* const { windowSize, networkStatus } = sensors(
|
|
2230
|
+
* ['windowSize', 'networkStatus'],
|
|
2231
|
+
* { windowSize: { throttle: 200 } },
|
|
2232
|
+
* );
|
|
2233
|
+
*
|
|
2234
|
+
* effect(() => console.log(windowSize(), networkStatus()));
|
|
2235
|
+
* ```
|
|
2236
|
+
*/
|
|
2042
2237
|
function sensors(track, opt) {
|
|
2043
2238
|
return track.reduce((result, key) => {
|
|
2044
2239
|
result[key] = sensor(key, opt?.[key]);
|