@mmstack/primitives 20.0.2 → 20.0.4
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 +40 -3
- package/fesm2022/mmstack-primitives.mjs +68 -106
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +58 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ Reactive map helper that stabilizes a source array Signal by length. It provides
|
|
|
217
217
|
|
|
218
218
|
```typescript
|
|
219
219
|
import { Component, signal } from '@angular/core';
|
|
220
|
-
import { mapArray } from '@mmstack/primitives';
|
|
220
|
+
import { mapArray, mutable } from '@mmstack/primitives';
|
|
221
221
|
|
|
222
222
|
@Component({
|
|
223
223
|
selector: 'app-map-demo',
|
|
@@ -225,14 +225,16 @@ import { mapArray } from '@mmstack/primitives';
|
|
|
225
225
|
<ul>
|
|
226
226
|
@for (item of displayItems(); track item) {
|
|
227
227
|
<li>{{ item() }}</li>
|
|
228
|
+
@if ($first) {
|
|
229
|
+
<button (click)="updateFirst(item)">Update First</button>
|
|
230
|
+
}
|
|
228
231
|
}
|
|
229
232
|
</ul>
|
|
230
233
|
<button (click)="addItem()">Add</button>
|
|
231
|
-
<button (click)="updateFirst()">Update First</button>
|
|
232
234
|
`,
|
|
233
235
|
})
|
|
234
236
|
export class ListComponent {
|
|
235
|
-
sourceItems = signal([
|
|
237
|
+
readonly sourceItems = signal([
|
|
236
238
|
{ id: 1, name: 'A' },
|
|
237
239
|
{ id: 2, name: 'B' },
|
|
238
240
|
]);
|
|
@@ -249,6 +251,41 @@ export class ListComponent {
|
|
|
249
251
|
return [...items]; // New array, but mapArray keeps stable signals
|
|
250
252
|
});
|
|
251
253
|
}
|
|
254
|
+
|
|
255
|
+
// since the underlying source is a signal we can also create updaters in the mapper
|
|
256
|
+
readonly updatableItems = mapArray(this.sourceItems, (child, index) => {
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
value: computed(() => `Item ${index}: ${child().name}`))
|
|
260
|
+
updateName: () => child.update((cur) => ({...cur, name: cur.name + '+'}))
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
// since the underlying source is a WritableSignal we can also create updaters in the mapper
|
|
266
|
+
readonly writableItems = mapArray(this.sourceItems, (child, index) => {
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
value: computed(() => `Item ${index}: ${child().name}`))
|
|
270
|
+
updateName: () => child.update((cur) => ({...cur, name: cur.name + '+'}))
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// if the source is a mutable signal we can even update them inline
|
|
275
|
+
readonly sourceItems = mutable([
|
|
276
|
+
{ id: 1, name: 'A' },
|
|
277
|
+
{ id: 2, name: 'B' },
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
readonly mutableItems = mapArray(this.sourceItems, (child, index) => {
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
value: computed(() => `Item ${index}: ${child().name}`))
|
|
284
|
+
updateName: () => child.inline((cur) => {
|
|
285
|
+
cur.name += '+';
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
});
|
|
252
289
|
}
|
|
253
290
|
```
|
|
254
291
|
|
|
@@ -391,77 +391,69 @@ function elementVisibility(target = inject(ElementRef), opt) {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* for mapped items. It receives a source function returning an array (or a Signal<T[]>)
|
|
398
|
-
* and a mapping function.
|
|
399
|
-
*
|
|
400
|
-
* For each item in the source array, it creates a stable `computed` signal representing
|
|
401
|
-
* that item's value at its current index. This stable signal (`Signal<T>`) is passed
|
|
402
|
-
* to the mapping function. This ensures that downstream computations or components
|
|
403
|
-
* depending on the mapped result only re-render or re-calculate for the specific items
|
|
404
|
-
* that have changed, or when items are added/removed, rather than re-evaluating everything
|
|
405
|
-
* when the source array reference changes but items remain the same.
|
|
406
|
-
*
|
|
407
|
-
* It efficiently handles changes in the source array's length by reusing existing mapped
|
|
408
|
-
* results when possible, slicing when the array shrinks, and appending new mapped items
|
|
409
|
-
* when it grows.
|
|
410
|
-
*
|
|
411
|
-
* @template T The type of items in the source array.
|
|
412
|
-
* @template U The type of items in the resulting mapped array.
|
|
413
|
-
*
|
|
414
|
-
* @param source A function returning the source array `T[]`, or a `Signal<T[]>` itself.
|
|
415
|
-
* The `mapArray` function will reactively update based on changes to this source.
|
|
416
|
-
* @param map The mapping function. It is called for each item in the source array.
|
|
417
|
-
* It receives:
|
|
418
|
-
* - `value`: A stable `Signal<T>` representing the item at the current index.
|
|
419
|
-
* Use this signal within your mapping logic if you need reactivity
|
|
420
|
-
* tied to the specific item's value changes.
|
|
421
|
-
* - `index`: The number index of the item in the array.
|
|
422
|
-
* It should return the mapped value `U`.
|
|
423
|
-
* @param [opt] Optional `CreateSignalOptions<T>`. These options are passed directly
|
|
424
|
-
* to the `computed` signal created for each individual item (`Signal<T>`).
|
|
425
|
-
* This allows specifying options like a custom `equal` function for item comparison.
|
|
426
|
-
*
|
|
427
|
-
* @returns A `Signal<U[]>` containing the mapped array. This signal updates whenever
|
|
428
|
-
* the source array changes (either length or the values of its items).
|
|
429
|
-
*
|
|
430
|
-
* @example
|
|
431
|
-
* ```ts
|
|
432
|
-
* const sourceItems = signal([
|
|
433
|
-
* { id: 1, name: 'Apple' },
|
|
434
|
-
* { id: 2, name: 'Banana' }
|
|
435
|
-
* ]);
|
|
436
|
-
*
|
|
437
|
-
* const mappedItems = mapArray(
|
|
438
|
-
* sourceItems,
|
|
439
|
-
* (itemSignal, index) => {
|
|
440
|
-
* // itemSignal is stable for a given item based on its index.
|
|
441
|
-
* // We create a computed here to react to changes in the item's name.
|
|
442
|
-
* return computed(() => `${index}: ${itemSignal().name.toUpperCase()}`);
|
|
443
|
-
* },
|
|
444
|
-
* // Example optional options (e.g., custom equality for item signals)
|
|
445
|
-
* { equal: (a, b) => a.id === b.id && a.name === b.name }
|
|
446
|
-
* );
|
|
447
|
-
* ```
|
|
448
|
-
* @remarks
|
|
449
|
-
* This function achieves its high performance by leveraging the new `linkedSignal`
|
|
450
|
-
* API from Angular, which allows for efficient memoization and reuse of array items.
|
|
394
|
+
* @internal
|
|
395
|
+
* Checks if a signal is a WritableSignal.
|
|
396
|
+
* @param sig The signal to check.
|
|
451
397
|
*/
|
|
452
|
-
function
|
|
398
|
+
function isWritable(sig) {
|
|
399
|
+
// We just need to check for the presence of a 'set' method.
|
|
400
|
+
return 'set' in sig;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* @internal
|
|
404
|
+
* Creates a setter function for a source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
405
|
+
* @param source The source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
406
|
+
* @returns
|
|
407
|
+
*/
|
|
408
|
+
function createSetter(source) {
|
|
409
|
+
if (!isWritable(source))
|
|
410
|
+
return () => {
|
|
411
|
+
// noop;
|
|
412
|
+
};
|
|
413
|
+
if (isMutable(source))
|
|
414
|
+
return (value, index) => {
|
|
415
|
+
source.inline((arr) => {
|
|
416
|
+
arr[index] = value;
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
return (value, index) => {
|
|
420
|
+
source.update((arr) => arr.map((v, i) => (i === index ? value : v)));
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function mapArray(source, map, options) {
|
|
453
424
|
const data = isSignal(source) ? source : computed(source);
|
|
454
425
|
const len = computed(() => data().length, ...(ngDevMode ? [{ debugName: "len" }] : []));
|
|
426
|
+
const setter = createSetter(data);
|
|
427
|
+
const opt = { ...options };
|
|
428
|
+
const writableData = isWritable(data)
|
|
429
|
+
? data
|
|
430
|
+
: toWritable(data, () => {
|
|
431
|
+
// noop
|
|
432
|
+
});
|
|
433
|
+
if (isWritable(data) && isMutable(data) && !opt.equal) {
|
|
434
|
+
opt.equal = (a, b) => {
|
|
435
|
+
if (a !== b)
|
|
436
|
+
return false; // actually check primitives and references
|
|
437
|
+
return false; // opt out for same refs
|
|
438
|
+
};
|
|
439
|
+
}
|
|
455
440
|
return linkedSignal({
|
|
456
441
|
source: () => len(),
|
|
457
442
|
computation: (len, prev) => {
|
|
458
443
|
if (!prev)
|
|
459
|
-
return Array.from({ length: len }, (_, i) =>
|
|
444
|
+
return Array.from({ length: len }, (_, i) => {
|
|
445
|
+
const derivation = derived(writableData, // typcase to largest type
|
|
446
|
+
{
|
|
447
|
+
from: (src) => src[i],
|
|
448
|
+
onChange: (value) => setter(value, i),
|
|
449
|
+
}, opt);
|
|
450
|
+
return map(derivation, i);
|
|
451
|
+
});
|
|
460
452
|
if (len === prev.value.length)
|
|
461
453
|
return prev.value;
|
|
462
454
|
if (len < prev.value.length) {
|
|
463
455
|
const slice = prev.value.slice(0, len);
|
|
464
|
-
if (opt
|
|
456
|
+
if (opt.onDestroy) {
|
|
465
457
|
for (let i = len; i < prev.value.length; i++) {
|
|
466
458
|
opt.onDestroy?.(prev.value[i]);
|
|
467
459
|
}
|
|
@@ -471,7 +463,12 @@ function mapArray(source, map, opt) {
|
|
|
471
463
|
else {
|
|
472
464
|
const next = [...prev.value];
|
|
473
465
|
for (let i = prev.value.length; i < len; i++) {
|
|
474
|
-
|
|
466
|
+
const derivation = derived(writableData, // typcase to largest type
|
|
467
|
+
{
|
|
468
|
+
from: (src) => src[i],
|
|
469
|
+
onChange: (value) => setter(value, i),
|
|
470
|
+
}, opt);
|
|
471
|
+
next[i] = map(derivation, i);
|
|
475
472
|
}
|
|
476
473
|
return next;
|
|
477
474
|
}
|
|
@@ -524,11 +521,11 @@ function mapArray(source, map, opt) {
|
|
|
524
521
|
* }
|
|
525
522
|
* ```
|
|
526
523
|
*/
|
|
527
|
-
function mediaQuery(query, debugName) {
|
|
524
|
+
function mediaQuery(query, debugName = 'mediaQuery') {
|
|
528
525
|
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
529
526
|
return computed(() => false, { debugName });
|
|
530
527
|
const mediaQueryList = window.matchMedia(query);
|
|
531
|
-
const state = signal(mediaQueryList.matches,
|
|
528
|
+
const state = signal(mediaQueryList.matches, { debugName: debugName });
|
|
532
529
|
const handleChange = (event) => {
|
|
533
530
|
state.set(event.matches);
|
|
534
531
|
};
|
|
@@ -712,12 +709,12 @@ function mousePosition(opt) {
|
|
|
712
709
|
x: 0,
|
|
713
710
|
y: 0,
|
|
714
711
|
}), {
|
|
715
|
-
debugName: opt?.debugName,
|
|
712
|
+
debugName: opt?.debugName ?? 'mousePosition',
|
|
716
713
|
});
|
|
717
714
|
base.unthrottled = base;
|
|
718
715
|
return base;
|
|
719
716
|
}
|
|
720
|
-
const { target = window, coordinateSpace = 'client', touch = false, debugName, throttle = 100, } = opt ?? {};
|
|
717
|
+
const { target = window, coordinateSpace = 'client', touch = false, debugName = 'mousePosition', throttle = 100, } = opt ?? {};
|
|
721
718
|
const eventTarget = target instanceof ElementRef ? target.nativeElement : target;
|
|
722
719
|
if (!eventTarget) {
|
|
723
720
|
if (isDevMode())
|
|
@@ -778,7 +775,7 @@ const serverDate = new Date();
|
|
|
778
775
|
* @param debugName Optional debug name for the signal.
|
|
779
776
|
* @returns A `NetworkStatusSignal` instance.
|
|
780
777
|
*/
|
|
781
|
-
function networkStatus(debugName) {
|
|
778
|
+
function networkStatus(debugName = 'networkStatus') {
|
|
782
779
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
783
780
|
const sig = computed(() => true, {
|
|
784
781
|
debugName,
|
|
@@ -845,7 +842,7 @@ function networkStatus(debugName) {
|
|
|
845
842
|
* }
|
|
846
843
|
* ```
|
|
847
844
|
*/
|
|
848
|
-
function pageVisibility(debugName) {
|
|
845
|
+
function pageVisibility(debugName = 'pageVisibility') {
|
|
849
846
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
850
847
|
return computed(() => 'visible', { debugName });
|
|
851
848
|
}
|
|
@@ -907,12 +904,12 @@ function scrollPosition(opt) {
|
|
|
907
904
|
x: 0,
|
|
908
905
|
y: 0,
|
|
909
906
|
}), {
|
|
910
|
-
debugName: opt?.debugName,
|
|
907
|
+
debugName: opt?.debugName ?? 'scrollPosition',
|
|
911
908
|
});
|
|
912
909
|
base.unthrottled = base;
|
|
913
910
|
return base;
|
|
914
911
|
}
|
|
915
|
-
const { target = window, throttle = 100, debugName } = opt || {};
|
|
912
|
+
const { target = window, throttle = 100, debugName = 'scrollPosition', } = opt || {};
|
|
916
913
|
let element;
|
|
917
914
|
let getScrollPosition;
|
|
918
915
|
if (target instanceof Window) {
|
|
@@ -998,12 +995,12 @@ function windowSize(opt) {
|
|
|
998
995
|
const base = computed(() => ({
|
|
999
996
|
width: 1024,
|
|
1000
997
|
height: 768,
|
|
1001
|
-
}), { debugName: opt?.debugName });
|
|
998
|
+
}), { debugName: opt?.debugName ?? 'windowSize' });
|
|
1002
999
|
base.unthrottled = base;
|
|
1003
1000
|
return base;
|
|
1004
1001
|
}
|
|
1005
1002
|
const sizeSignal = throttled({ width: window.innerWidth, height: window.innerHeight }, {
|
|
1006
|
-
debugName: opt?.debugName,
|
|
1003
|
+
debugName: opt?.debugName ?? 'windowSize',
|
|
1007
1004
|
equal: (a, b) => a.width === b.width && a.height === b.height,
|
|
1008
1005
|
ms: opt?.throttle ?? 100,
|
|
1009
1006
|
});
|
|
@@ -1211,41 +1208,6 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1211
1208
|
return writable;
|
|
1212
1209
|
}
|
|
1213
1210
|
|
|
1214
|
-
/**
|
|
1215
|
-
* Creates a Promise that resolves when a signal's value satisfies a given predicate.
|
|
1216
|
-
*
|
|
1217
|
-
* This is useful for imperatively waiting for a reactive state to change,
|
|
1218
|
-
* for example, in tests or to orchestrate complex asynchronous operations.
|
|
1219
|
-
*
|
|
1220
|
-
* @template T The type of the signal's value.
|
|
1221
|
-
* @param sourceSignal The signal to observe.
|
|
1222
|
-
* @param predicate A function that takes the signal's value and returns `true` if the condition is met.
|
|
1223
|
-
* @param options Optional configuration for timeout and explicit destruction.
|
|
1224
|
-
* @returns A Promise that resolves with the signal's value when the predicate is true,
|
|
1225
|
-
* or rejects on timeout or context destruction.
|
|
1226
|
-
*
|
|
1227
|
-
* @example
|
|
1228
|
-
* ```ts
|
|
1229
|
-
* const count = signal(0);
|
|
1230
|
-
*
|
|
1231
|
-
* async function waitForCount() {
|
|
1232
|
-
* console.log('Waiting for count to be >= 3...');
|
|
1233
|
-
* try {
|
|
1234
|
-
* const finalCount = await until(count, c => c >= 3, { timeout: 5000 });
|
|
1235
|
-
* console.log(`Count reached: ${finalCount}`);
|
|
1236
|
-
* } catch (e: any) { // Ensure 'e' is typed if you access properties like e.message
|
|
1237
|
-
* console.error(e.message); // e.g., "until: Timeout after 5000ms."
|
|
1238
|
-
* }
|
|
1239
|
-
* }
|
|
1240
|
-
*
|
|
1241
|
-
* // Simulate updates
|
|
1242
|
-
* setTimeout(() => count.set(1), 500);
|
|
1243
|
-
* setTimeout(() => count.set(2), 1000);
|
|
1244
|
-
* setTimeout(() => count.set(3), 1500);
|
|
1245
|
-
*
|
|
1246
|
-
* waitForCount();
|
|
1247
|
-
* ```
|
|
1248
|
-
*/
|
|
1249
1211
|
function until(sourceSignal, predicate, options = {}) {
|
|
1250
1212
|
const injector = options.injector ?? inject(Injector);
|
|
1251
1213
|
return new Promise((resolve, reject) => {
|