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