@jscutlery/rx-computed 0.1.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 +82 -0
- package/esm2022/index.mjs +2 -0
- package/esm2022/jscutlery-rx-computed.mjs +5 -0
- package/esm2022/lib/rx-computed.mjs +22 -0
- package/fesm2022/jscutlery-rx-computed.mjs +29 -0
- package/fesm2022/jscutlery-rx-computed.mjs.map +1 -0
- package/index.d.ts +1 -0
- package/lib/rx-computed.d.ts +5 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# `@jscutlery/rx-computed`
|
|
2
|
+
|
|
3
|
+
# Installation
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
yarn add @jscutlery/rx-computed
|
|
7
|
+
|
|
8
|
+
# or
|
|
9
|
+
|
|
10
|
+
npm install @jscutlery/rx-computed
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
# `rxComputed`
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
```typescript
|
|
17
|
+
import { rxComputed } from '@jscutlery/rx-computed';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
...
|
|
21
|
+
template: `
|
|
22
|
+
<mc-keywords-input (keywordsChange)="keywords.set($event)"/>
|
|
23
|
+
<mc-sort-input (sortChange)="sort.set($event)"/>
|
|
24
|
+
<mc-recipes-list [recipes]="recipes()"/>
|
|
25
|
+
`
|
|
26
|
+
})
|
|
27
|
+
class MyCmp {
|
|
28
|
+
keywords = signal<string | undefined>(undefined);
|
|
29
|
+
sort = signal<'asc' | 'desc'>('desc');
|
|
30
|
+
|
|
31
|
+
recipes = rxComputed(() => repo.getRecipes(keywords(), sort()), {initialValue: []});
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Motivation
|
|
36
|
+
|
|
37
|
+
Synchronously computed signals in Angular are relatively straight forward. However, when dealing with an asynchronous source of data like an `Observable`, there is no primitive to derive a signal from it.
|
|
38
|
+
|
|
39
|
+
There are two common ways of dealing with this:
|
|
40
|
+
|
|
41
|
+
1. Using `@angular/core/rxjs-interop` which requires us to explicitly define the dependencies of the computed signal:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Signals
|
|
45
|
+
const keywords = signal(...);
|
|
46
|
+
const sort = signal(...);
|
|
47
|
+
|
|
48
|
+
// Signals => RxJS
|
|
49
|
+
const recipes$ = combineLatest({
|
|
50
|
+
keywords: toObservable(keywords),
|
|
51
|
+
sort: toObservable(sort)
|
|
52
|
+
}).pipe(
|
|
53
|
+
switchMap(({ keywords, sort }) => repo.getRecipes(keywords, sort))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// RxJS => Signals
|
|
57
|
+
const recipes = toSignal(recipes$, []);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
2. Using `effet` explicitly _(which is not the recommended way of using it, cf. https://angular.io/guide/signals#when-not-to-use-effects)_:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const keywords = signal(...);
|
|
64
|
+
const sort = signal(...);
|
|
65
|
+
|
|
66
|
+
const recipes = signal(...);
|
|
67
|
+
|
|
68
|
+
effect((onCleanup) => {
|
|
69
|
+
const sub = repo.getRecipes(keywords(), sort())
|
|
70
|
+
.subscribe(_recipes => recipes.set(_recipes));
|
|
71
|
+
|
|
72
|
+
onCleanup(() => sub.unsubscribe());
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## FAQ
|
|
77
|
+
|
|
78
|
+
### What about errors?
|
|
79
|
+
|
|
80
|
+
Cf. [Managing RxJS Traffic with Signals and Suspensify](https://marmicode.io/blog/managing-rxjs-traffic-with-signals)
|
|
81
|
+
|
|
82
|
+
Cf. [@jscutlery/operators#suspensify](../operators/README.md#suspensify)
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { rxComputed } from './lib/rx-computed';
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9wYWNrYWdlcy9yeC1jb21wdXRlZC9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG1CQUFtQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IHsgcnhDb21wdXRlZCB9IGZyb20gJy4vbGliL3J4LWNvbXB1dGVkJzsiXX0=
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated bundle index. Do not edit.
|
|
3
|
+
*/
|
|
4
|
+
export * from './index';
|
|
5
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNjdXRsZXJ5LXJ4LWNvbXB1dGVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcGFja2FnZXMvcngtY29tcHV0ZWQvc3JjL2pzY3V0bGVyeS1yeC1jb21wdXRlZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUVILGNBQWMsU0FBUyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBHZW5lcmF0ZWQgYnVuZGxlIGluZGV4LiBEbyBub3QgZWRpdC5cbiAqL1xuXG5leHBvcnQgKiBmcm9tICcuL2luZGV4JztcbiJdfQ==
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { computed, effect, signal } from '@angular/core';
|
|
2
|
+
import { pending, suspensify } from '@jscutlery/operators';
|
|
3
|
+
export function rxComputed(fn, { initialValue } = {}) {
|
|
4
|
+
const sig = signal(pending);
|
|
5
|
+
effect((onCleanup) => {
|
|
6
|
+
const sub = fn()
|
|
7
|
+
.pipe(suspensify())
|
|
8
|
+
.subscribe((value) => sig.set(value));
|
|
9
|
+
onCleanup(() => sub.unsubscribe());
|
|
10
|
+
}, { allowSignalWrites: true });
|
|
11
|
+
return computed(() => {
|
|
12
|
+
const suspense = sig();
|
|
13
|
+
if (suspense.hasError) {
|
|
14
|
+
throw suspense.error;
|
|
15
|
+
}
|
|
16
|
+
if (suspense.hasValue) {
|
|
17
|
+
return suspense.value;
|
|
18
|
+
}
|
|
19
|
+
return initialValue;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicngtY29tcHV0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9wYWNrYWdlcy9yeC1jb21wdXRlZC9zcmMvbGliL3J4LWNvbXB1dGVkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBVSxNQUFNLGVBQWUsQ0FBQztBQUNqRSxPQUFPLEVBQUUsT0FBTyxFQUFZLFVBQVUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBR3JFLE1BQU0sVUFBVSxVQUFVLENBQ3hCLEVBQXVCLEVBQ3ZCLEVBQUUsWUFBWSxLQUF1QyxFQUFFO0lBRXZELE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBOEIsT0FBTyxDQUFDLENBQUM7SUFFekQsTUFBTSxDQUNKLENBQUMsU0FBUyxFQUFFLEVBQUU7UUFDWixNQUFNLEdBQUcsR0FBRyxFQUFFLEVBQUU7YUFDYixJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7YUFDbEIsU0FBUyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDeEMsU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0lBQ3JDLENBQUMsRUFDRCxFQUFFLGlCQUFpQixFQUFFLElBQUksRUFBRSxDQUM1QixDQUFDO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sUUFBUSxHQUFHLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLElBQUksUUFBUSxDQUFDLFFBQVEsRUFBRTtZQUNyQixNQUFNLFFBQVEsQ0FBQyxLQUFLLENBQUM7U0FDdEI7UUFFRCxJQUFJLFFBQVEsQ0FBQyxRQUFRLEVBQUU7WUFDckIsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDO1NBQ3ZCO1FBRUQsT0FBTyxZQUE2QixDQUFDO0lBQ3ZDLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNvbXB1dGVkLCBlZmZlY3QsIHNpZ25hbCwgU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBwZW5kaW5nLCBTdXNwZW5zZSwgc3VzcGVuc2lmeSB9IGZyb20gJ0Bqc2N1dGxlcnkvb3BlcmF0b3JzJztcbmltcG9ydCB7IE9ic2VydmFibGUgfSBmcm9tICdyeGpzJztcblxuZXhwb3J0IGZ1bmN0aW9uIHJ4Q29tcHV0ZWQ8VCwgSU5JVElBTF9WQUxVRSA9IFQgfCBudWxsIHwgdW5kZWZpbmVkPihcbiAgZm46ICgpID0+IE9ic2VydmFibGU8VD4sXG4gIHsgaW5pdGlhbFZhbHVlIH06IHsgaW5pdGlhbFZhbHVlPzogSU5JVElBTF9WQUxVRSB9ID0ge31cbik6IFNpZ25hbDxUIHwgSU5JVElBTF9WQUxVRT4ge1xuICBjb25zdCBzaWcgPSBzaWduYWw8U3VzcGVuc2U8VCB8IElOSVRJQUxfVkFMVUU+PihwZW5kaW5nKTtcblxuICBlZmZlY3QoXG4gICAgKG9uQ2xlYW51cCkgPT4ge1xuICAgICAgY29uc3Qgc3ViID0gZm4oKVxuICAgICAgICAucGlwZShzdXNwZW5zaWZ5KCkpXG4gICAgICAgIC5zdWJzY3JpYmUoKHZhbHVlKSA9PiBzaWcuc2V0KHZhbHVlKSk7XG4gICAgICBvbkNsZWFudXAoKCkgPT4gc3ViLnVuc3Vic2NyaWJlKCkpO1xuICAgIH0sXG4gICAgeyBhbGxvd1NpZ25hbFdyaXRlczogdHJ1ZSB9XG4gICk7XG5cbiAgcmV0dXJuIGNvbXB1dGVkKCgpID0+IHtcbiAgICBjb25zdCBzdXNwZW5zZSA9IHNpZygpO1xuICAgIGlmIChzdXNwZW5zZS5oYXNFcnJvcikge1xuICAgICAgdGhyb3cgc3VzcGVuc2UuZXJyb3I7XG4gICAgfVxuXG4gICAgaWYgKHN1c3BlbnNlLmhhc1ZhbHVlKSB7XG4gICAgICByZXR1cm4gc3VzcGVuc2UudmFsdWU7XG4gICAgfVxuXG4gICAgcmV0dXJuIGluaXRpYWxWYWx1ZSBhcyBJTklUSUFMX1ZBTFVFO1xuICB9KTtcbn0iXX0=
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { signal, effect, computed } from '@angular/core';
|
|
2
|
+
import { pending, suspensify } from '@jscutlery/operators';
|
|
3
|
+
|
|
4
|
+
function rxComputed(fn, { initialValue } = {}) {
|
|
5
|
+
const sig = signal(pending);
|
|
6
|
+
effect((onCleanup) => {
|
|
7
|
+
const sub = fn()
|
|
8
|
+
.pipe(suspensify())
|
|
9
|
+
.subscribe((value) => sig.set(value));
|
|
10
|
+
onCleanup(() => sub.unsubscribe());
|
|
11
|
+
}, { allowSignalWrites: true });
|
|
12
|
+
return computed(() => {
|
|
13
|
+
const suspense = sig();
|
|
14
|
+
if (suspense.hasError) {
|
|
15
|
+
throw suspense.error;
|
|
16
|
+
}
|
|
17
|
+
if (suspense.hasValue) {
|
|
18
|
+
return suspense.value;
|
|
19
|
+
}
|
|
20
|
+
return initialValue;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generated bundle index. Do not edit.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export { rxComputed };
|
|
29
|
+
//# sourceMappingURL=jscutlery-rx-computed.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jscutlery-rx-computed.mjs","sources":["../../../../packages/rx-computed/src/lib/rx-computed.ts","../../../../packages/rx-computed/src/jscutlery-rx-computed.ts"],"sourcesContent":["import { computed, effect, signal, Signal } from '@angular/core';\nimport { pending, Suspense, suspensify } from '@jscutlery/operators';\nimport { Observable } from 'rxjs';\n\nexport function rxComputed<T, INITIAL_VALUE = T | null | undefined>(\n fn: () => Observable<T>,\n { initialValue }: { initialValue?: INITIAL_VALUE } = {}\n): Signal<T | INITIAL_VALUE> {\n const sig = signal<Suspense<T | INITIAL_VALUE>>(pending);\n\n effect(\n (onCleanup) => {\n const sub = fn()\n .pipe(suspensify())\n .subscribe((value) => sig.set(value));\n onCleanup(() => sub.unsubscribe());\n },\n { allowSignalWrites: true }\n );\n\n return computed(() => {\n const suspense = sig();\n if (suspense.hasError) {\n throw suspense.error;\n }\n\n if (suspense.hasValue) {\n return suspense.value;\n }\n\n return initialValue as INITIAL_VALUE;\n });\n}","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAIM,SAAU,UAAU,CACxB,EAAuB,EACvB,EAAE,YAAY,KAAuC,EAAE,EAAA;AAEvD,IAAA,MAAM,GAAG,GAAG,MAAM,CAA8B,OAAO,CAAC,CAAC;AAEzD,IAAA,MAAM,CACJ,CAAC,SAAS,KAAI;QACZ,MAAM,GAAG,GAAG,EAAE,EAAE;aACb,IAAI,CAAC,UAAU,EAAE,CAAC;AAClB,aAAA,SAAS,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,SAAS,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;AACrC,KAAC,EACD,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAC5B,CAAC;IAEF,OAAO,QAAQ,CAAC,MAAK;AACnB,QAAA,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC;QACvB,IAAI,QAAQ,CAAC,QAAQ,EAAE;YACrB,MAAM,QAAQ,CAAC,KAAK,CAAC;AACtB,SAAA;QAED,IAAI,QAAQ,CAAC,QAAQ,EAAE;YACrB,OAAO,QAAQ,CAAC,KAAK,CAAC;AACvB,SAAA;AAED,QAAA,OAAO,YAA6B,CAAC;AACvC,KAAC,CAAC,CAAC;AACL;;AChCA;;AAEG;;;;"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { rxComputed } from './lib/rx-computed';
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jscutlery/rx-computed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@angular/common": "^16.0.0",
|
|
6
|
+
"@angular/core": "^16.0.0",
|
|
7
|
+
"rxjs": "7.8.1",
|
|
8
|
+
"@jscutlery/operators": "2.1.2"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"tslib": "^2.4.0"
|
|
12
|
+
},
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"module": "fesm2022/jscutlery-rx-computed.mjs",
|
|
15
|
+
"typings": "index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
"./package.json": {
|
|
18
|
+
"default": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./index.d.ts",
|
|
22
|
+
"esm2022": "./esm2022/jscutlery-rx-computed.mjs",
|
|
23
|
+
"esm": "./esm2022/jscutlery-rx-computed.mjs",
|
|
24
|
+
"default": "./fesm2022/jscutlery-rx-computed.mjs"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|