flurryx 0.7.6 → 0.8.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 +89 -25
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -29,18 +29,37 @@ interface ProductStoreConfig {
|
|
|
29
29
|
export const ProductStore = Store.for<ProductStoreConfig>().build();
|
|
30
30
|
|
|
31
31
|
// Facade
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
32
|
+
@Injectable()
|
|
33
|
+
export class ProductFacade {
|
|
34
|
+
@SkipIfCached("LIST", (i) => i.store)
|
|
35
|
+
@Loading("LIST", (i) => i.store)
|
|
36
|
+
loadProducts() {
|
|
37
|
+
this.http
|
|
38
|
+
.get<Product[]>("/api/products")
|
|
39
|
+
.pipe(syncToStore(this.store, "LIST"))
|
|
40
|
+
.subscribe();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read signals from the facade
|
|
44
|
+
getProducts() {
|
|
45
|
+
return this.store.get("LIST");
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
// Component
|
|
41
|
-
@
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// Component — read the facade signal once, use it in the template
|
|
50
|
+
@Component({
|
|
51
|
+
selector: "app-product-list",
|
|
52
|
+
template: `
|
|
53
|
+
@if (productsState().isLoading) {
|
|
54
|
+
<spinner />
|
|
55
|
+
} @for (product of productsState().data; track product.id) {
|
|
56
|
+
<product-card [product]="product" />
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
})
|
|
60
|
+
export class ProductListComponent {
|
|
61
|
+
private readonly facade = inject(ProductFacade);
|
|
62
|
+
readonly productsState = this.facade.getProducts();
|
|
44
63
|
}
|
|
45
64
|
```
|
|
46
65
|
|
|
@@ -67,6 +86,7 @@ No `async` pipe. No `subscribe` in templates. No manual unsubscription.
|
|
|
67
86
|
- [Keyed Resources](#keyed-resources)
|
|
68
87
|
- [Store Mirroring](#store-mirroring)
|
|
69
88
|
- [Builder .mirror()](#builder-mirror)
|
|
89
|
+
- [Builder .mirrorSelf()](#builder-mirrorself)
|
|
70
90
|
- [Builder .mirrorKeyed()](#builder-mirrorkeyed)
|
|
71
91
|
- [mirrorKey](#mirrorkey)
|
|
72
92
|
- [collectKeyed](#collectkeyed)
|
|
@@ -174,29 +194,33 @@ import { Injectable, inject } from "@angular/core";
|
|
|
174
194
|
import { HttpClient } from "@angular/common/http";
|
|
175
195
|
import { syncToStore, SkipIfCached, Loading } from "flurryx";
|
|
176
196
|
|
|
177
|
-
@Injectable(
|
|
197
|
+
@Injectable()
|
|
178
198
|
export class ProductFacade {
|
|
179
199
|
private readonly http = inject(HttpClient);
|
|
180
200
|
readonly store = inject(ProductStore);
|
|
181
201
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
getProducts() {
|
|
203
|
+
return this.store.get("LIST");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getProductDetail() {
|
|
207
|
+
return this.store.get("DETAIL");
|
|
208
|
+
}
|
|
185
209
|
|
|
186
|
-
@SkipIfCached(
|
|
187
|
-
@Loading(
|
|
210
|
+
@SkipIfCached("LIST", (i: ProductFacade) => i.store)
|
|
211
|
+
@Loading("LIST", (i: ProductFacade) => i.store)
|
|
188
212
|
loadProducts() {
|
|
189
213
|
this.http
|
|
190
214
|
.get<Product[]>("/api/products")
|
|
191
|
-
.pipe(syncToStore(this.store,
|
|
215
|
+
.pipe(syncToStore(this.store, "LIST"))
|
|
192
216
|
.subscribe();
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
@Loading(
|
|
219
|
+
@Loading("DETAIL", (i: ProductFacade) => i.store)
|
|
196
220
|
loadProduct(id: string) {
|
|
197
221
|
this.http
|
|
198
222
|
.get<Product>(`/api/products/${id}`)
|
|
199
|
-
.pipe(syncToStore(this.store,
|
|
223
|
+
.pipe(syncToStore(this.store, "DETAIL"))
|
|
200
224
|
.subscribe();
|
|
201
225
|
}
|
|
202
226
|
}
|
|
@@ -207,18 +231,19 @@ export class ProductFacade {
|
|
|
207
231
|
```typescript
|
|
208
232
|
@Component({
|
|
209
233
|
template: `
|
|
210
|
-
@if (
|
|
234
|
+
@if (productsState().isLoading) {
|
|
211
235
|
<spinner />
|
|
212
|
-
} @if (
|
|
213
|
-
|
|
236
|
+
} @if (productsState().status === 'Success') { @for (product of
|
|
237
|
+
productsState().data; track product.id) {
|
|
214
238
|
<product-card [product]="product" />
|
|
215
|
-
} } @if (
|
|
216
|
-
<error-banner [errors]="
|
|
239
|
+
} } @if (productsState().status === 'Error') {
|
|
240
|
+
<error-banner [errors]="productsState().errors" />
|
|
217
241
|
}
|
|
218
242
|
`,
|
|
219
243
|
})
|
|
220
244
|
export class ProductListComponent {
|
|
221
|
-
readonly facade = inject(ProductFacade);
|
|
245
|
+
private readonly facade = inject(ProductFacade);
|
|
246
|
+
readonly productsState = this.facade.getProducts();
|
|
222
247
|
|
|
223
248
|
constructor() {
|
|
224
249
|
this.facade.loadProducts();
|
|
@@ -696,6 +721,45 @@ export const SessionStore = Store.for<{ ARTICLES: Item[] }>()
|
|
|
696
721
|
|
|
697
722
|
The builder calls `inject()` under the hood, so source stores are resolved through Angular's DI. Everything — data, loading, status, errors — is mirrored automatically. No manual cleanup needed; the mirrors live as long as the store.
|
|
698
723
|
|
|
724
|
+
### Builder .mirrorSelf()
|
|
725
|
+
|
|
726
|
+
Use `.mirrorSelf()` when one slot in a store should mirror another slot in the same store. It is useful for aliases, local snapshots, or secondary slots that should stay in sync with a primary slot without wiring `onUpdate` manually.
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
interface SessionStoreConfig {
|
|
730
|
+
CUSTOMER_DETAILS: Customer;
|
|
731
|
+
CUSTOMER_SNAPSHOT: Customer;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
735
|
+
.mirrorSelf('CUSTOMER_DETAILS', 'CUSTOMER_SNAPSHOT')
|
|
736
|
+
.build();
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
It mirrors the full resource state one way — `data`, `isLoading`, `status`, and `errors` all flow from the source key to the target key. The target key must be different from the source key.
|
|
740
|
+
|
|
741
|
+
Because it listens to updates on the built store itself, `.mirrorSelf()` also reacts when the source key is updated by another mirror:
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
interface CustomerStoreConfig {
|
|
745
|
+
CUSTOMERS: Customer[];
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
interface SessionStoreConfig {
|
|
749
|
+
CUSTOMERS: Customer[];
|
|
750
|
+
CUSTOMER_COPY: Customer[];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
754
|
+
|
|
755
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
756
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
757
|
+
.mirrorSelf('CUSTOMERS', 'CUSTOMER_COPY')
|
|
758
|
+
.build();
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`.mirrorSelf()` is available on all builder styles. For fluent builders, declare both slots first, then chain `.mirrorSelf(sourceKey, targetKey)` before `.build()`.
|
|
762
|
+
|
|
699
763
|
### Builder .mirrorKeyed()
|
|
700
764
|
|
|
701
765
|
When the source store holds a single-entity slot (e.g. `CUSTOMER_DETAILS: Customer`) and you want to accumulate those fetches into a `KeyedResourceData` cache on the target, use `.mirrorKeyed()`. It is the builder equivalent of [`collectKeyed`](#collectkeyed).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flurryx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Signal-first reactive state management for Angular",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"sideEffects": false,
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flurryx/core": "0.
|
|
42
|
-
"@flurryx/store": "0.
|
|
43
|
-
"@flurryx/rx": "0.
|
|
41
|
+
"@flurryx/core": "0.8.0",
|
|
42
|
+
"@flurryx/store": "0.8.0",
|
|
43
|
+
"@flurryx/rx": "0.8.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@angular/core": ">=17.0.0",
|