@mmstack/primitives 20.4.2 → 20.4.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 CHANGED
@@ -214,40 +214,30 @@ export class ThemeSelectorComponent {
214
214
 
215
215
  ### piped
216
216
 
217
- Adds a chainable .pipe(...) method to signals, allowing you to compose pure, synchronous transforms into reactive pipelines. Each .pipe(...) call returns a computed signal that is itself pipeable, so you can keep chaining.
217
+ Adds two fluent APIs to signals:
218
218
 
219
- ```typescript
220
- import { Component, effect } from '@angular/core';
221
- import { piped, pipeable } from '@mmstack/primitives';
219
+ - **`.map(...transforms, [options])`** – compose pure, synchronous value→value transforms. Returns a computed signal that remains pipeable.
220
+ - **`.pipe(...operators)`** – compose operators (signal→signal), useful for combining signals or reusable projections.
222
221
 
223
- @Component({
224
- selector: 'app-pipeable',
225
- template: `<button (click)="increment()">Increment</button>`,
226
- })
227
- export class PipeableComponent {
228
- count = piped(1);
222
+ ```typescript
223
+ import { piped, pipeable, select, combineWith } from '@mmstack/primitives';
224
+ import { signal } from '@angular/core';
229
225
 
230
- // Create a derived pipeline
231
- label = this.count.pipe(
232
- (n) => n * 2, // number -> number
233
- (n) => `#${n}`, // number -> string
234
- );
226
+ const count = piped(1);
235
227
 
236
- constructor() {
237
- effect(() => {
238
- console.log('Label:', this.label()); // e.g., "#2"
239
- });
240
- }
228
+ // Map: value -> value
229
+ const label = count.map(
230
+ (n) => n * 2,
231
+ (n) => (num: n),
232
+ { equal: (a, b) => a.num === b.num },
233
+ );
241
234
 
242
- increment() {
243
- this.count.update((n) => n + 1);
244
- }
245
- }
235
+ // Pipe: signal -> signal
236
+ const base = pipeable(signal(10));
237
+ const total = count.pipe(select((n) => n * 3)).pipe(combineWith(count, (a, b) => a + b));
246
238
 
247
- // You can also transform existing signals into pipable versions
248
- const example = pipeable(computed(() => 1)); // PipeableSignal<number> (a readonly signal + pipe)
249
- const example2 = pipeable(signal(1)); // PipeableSignal<number, WritableSignal<number>> (a writable signal + pipe)
250
- const example3 = pipeable(mutable({ name: 'john' })); // This returns a pipeable mutable signal (you get the point :) )
239
+ label(); // e.g., "#2"
240
+ total(); // reactive sum
251
241
  ```
252
242
 
253
243
  ### mapArray
@@ -477,6 +477,91 @@ function mapArray(source, map, options) {
477
477
  });
478
478
  }
479
479
 
480
+ /** Project with optional equality. Pure & sync. */
481
+ const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
482
+ /** Combine with another signal using a projector. */
483
+ const combineWith = (other, project, opt) => (src) => computed(() => project(src(), other()), opt);
484
+ /** Only re-emit when equal(prev, next) is false. */
485
+ const distinct = (equal = Object.is) => (src) => computed(() => src(), { equal });
486
+ /** map to new value */
487
+ const map = (fn) => (src) => computed(() => fn(src()));
488
+ /** filter values, keeping the last value if it was ever available, if first value is filtered will return undefined */
489
+ const filter = (predicate) => (src) => linkedSignal({
490
+ source: src,
491
+ computation: (next, prev) => {
492
+ if (predicate(next))
493
+ return next;
494
+ return prev?.source;
495
+ },
496
+ });
497
+ /** tap into the value */
498
+ const tap = (fn) => (src) => {
499
+ effect(() => fn(src()));
500
+ return src;
501
+ };
502
+
503
+ /**
504
+ * Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
505
+ *
506
+ * @example
507
+ * const s = pipeable(signal(1)); // WritableSignal<number> (+ pipe)
508
+ * const label = s.pipe(n => n * 2, n => `#${n}`); // Signal<string> (+ pipe)
509
+ * label(); // "#2"
510
+ */
511
+ function pipeable(signal) {
512
+ const internal = signal;
513
+ const mapImpl = (...fns) => {
514
+ const last = fns.at(-1);
515
+ let opt;
516
+ if (last && typeof last !== 'function') {
517
+ fns = fns.slice(0, -1);
518
+ opt = last;
519
+ }
520
+ if (fns.length === 0)
521
+ return internal;
522
+ if (fns.length === 1) {
523
+ const fn = fns[0];
524
+ return pipeable(computed(() => fn(internal()), opt));
525
+ }
526
+ const transformer = (input) => fns.reduce((acc, fn) => fn(acc), input);
527
+ return pipeable(computed(() => transformer(internal()), opt));
528
+ };
529
+ const pipeImpl = (...ops) => {
530
+ if (ops.length === 0)
531
+ return internal;
532
+ return ops.reduce((src, op) => pipeable(op(src)), internal);
533
+ };
534
+ Object.defineProperties(internal, {
535
+ map: {
536
+ value: mapImpl,
537
+ configurable: true,
538
+ enumerable: false,
539
+ writable: false,
540
+ },
541
+ pipe: {
542
+ value: pipeImpl,
543
+ configurable: true,
544
+ enumerable: false,
545
+ writable: false,
546
+ },
547
+ });
548
+ return internal;
549
+ }
550
+ /**
551
+ * Create a new **writable** signal and return it as a `PipableSignal`.
552
+ *
553
+ * The returned value is a `WritableSignal<T>` with `.set`, `.update`, `.asReadonly`
554
+ * still available (via intersection type), plus a chainable `.pipe(...)`.
555
+ *
556
+ * @example
557
+ * const count = piped(1); // WritableSignal<number> (+ pipe)
558
+ * const even = count.pipe(n => n % 2 === 0); // Signal<boolean> (+ pipe)
559
+ * count.update(n => n + 1);
560
+ */
561
+ function piped(initial, opt) {
562
+ return pipeable(signal(initial, opt));
563
+ }
564
+
480
565
  /**
481
566
  * Creates a read-only signal that reactively tracks whether a CSS media query
482
567
  * string currently matches.
@@ -1406,5 +1491,5 @@ function withHistory(source, opt) {
1406
1491
  * Generated bundle index. Do not edit.
1407
1492
  */
1408
1493
 
1409
- export { debounce, debounced, derived, elementVisibility, isDerivation, isMutable, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, prefersDarkMode, prefersReducedMotion, scrollPosition, sensor, stored, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
1494
+ export { combineWith, debounce, debounced, derived, distinct, elementVisibility, filter, isDerivation, isMutable, map, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, stored, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
1410
1495
  //# sourceMappingURL=mmstack-primitives.mjs.map