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