@mmstack/primitives 20.0.3 → 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 +58 -96
- 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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
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.
|
|
397
|
+
*/
|
|
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
|
|
451
407
|
*/
|
|
452
|
-
function
|
|
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
|
}
|
|
@@ -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) => {
|