@mmstack/primitives 20.0.3 → 20.3.0

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 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
- * Reactively maps items from a source array (or signal of an array) using a provided mapping function.
395
- *
396
- * This function serves a similar purpose to SolidJS's `mapArray` by providing stability
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.
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 mapArray(source, map, opt) {
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) => map(computed(() => source()[i], opt), 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?.onDestroy) {
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
- next[i] = map(computed(() => source()[i], opt), i);
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) => {