Skip to content

Commit 997b3ef

Browse files
committed
feat: add with-connect feature
withConnect() adds a method to connect Signals back to the store. Everytime these Signals change, the store will be updated accordingly.
1 parent 01172da commit 997b3ef

File tree

5 files changed

+147
-0
lines changed

5 files changed

+147
-0
lines changed

docs/docs/with-connect.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
title: withConnect()
3+
---
4+
5+
```typescript
6+
import { withConnect } from '@angular-architects/ngrx-toolkit';
7+
```
8+
9+
`withConnect()` adds a method to connect Signals back to your store. Everytime these Signals change, the store will be updated accordingly.
10+
11+
Example:
12+
13+
```ts
14+
const Store = signalStore(
15+
{ protectedState: false },
16+
withState({
17+
maxWarpFactor: 8,
18+
shipName: 'USS Enterprise',
19+
registration: 'NCC-1701',
20+
poeple: 430,
21+
}),
22+
withConnect()
23+
);
24+
```
25+
26+
```ts
27+
@Component({ ... })
28+
export class MyComponent {
29+
private store = inject(OfferListStore);
30+
31+
readonly maxWarpFactor = signal(8);
32+
readonly registration = signal('NCC-1701');
33+
readonly people = signal(430);
34+
35+
constructor() {
36+
//
37+
// Every change in the local Signals is
38+
// refected in the store
39+
//
40+
this.store.connect(() => ({
41+
//
42+
// Subset of state in the store
43+
//
44+
maxWarpFactor: this.maxWarpFactor(),
45+
registration: this.registration(),
46+
poeple: this.poeple(),
47+
}));
48+
}
49+
50+
...
51+
}
52+
```

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const sidebars: SidebarsConfig = {
2626
'with-feature-factory',
2727
'with-conditional',
2828
'with-call-state',
29+
'with-connect',
2930
],
3031
reduxConnectorSidebar: [
3132
{

libs/ngrx-toolkit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ export {
4141
} from './lib/storage-sync/with-storage-sync';
4242
export { emptyFeature, withConditional } from './lib/with-conditional';
4343
export { withFeatureFactory } from './lib/with-feature-factory';
44+
export * from './lib/with-connect';
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { getState, patchState, signalStore, withState } from '@ngrx/signals';
2+
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
3+
import { signal } from '@angular/core';
4+
import { withConnect } from './with-connect';
5+
6+
describe('withConnect', () => {
7+
const setup = () => {
8+
const Store = signalStore(
9+
{ protectedState: false },
10+
withState({
11+
maxWarpFactor: 8,
12+
shipName: 'USS Enterprise',
13+
registration: 'NCC-1701',
14+
poeple: 430,
15+
}),
16+
withConnect()
17+
);
18+
19+
const store = TestBed.configureTestingModule({
20+
providers: [Store],
21+
}).inject(Store);
22+
23+
return { store };
24+
};
25+
26+
it('should update store after connected signals are changed', fakeAsync(() => {
27+
const { store } = setup();
28+
29+
TestBed.runInInjectionContext(() => {
30+
const maxWarpFactor = signal(9);
31+
const registration = signal('NCC-1701-D');
32+
const poeple = signal(1100);
33+
34+
store.connect(() => ({
35+
maxWarpFactor: maxWarpFactor(),
36+
registration: registration(),
37+
poeple: poeple(),
38+
}));
39+
tick(1);
40+
expect(getState(store)).toMatchObject({
41+
maxWarpFactor: 9,
42+
shipName: 'USS Enterprise',
43+
registration: 'NCC-1701-D',
44+
poeple: 1100,
45+
});
46+
47+
maxWarpFactor.set(9.6);
48+
tick(1);
49+
expect(getState(store).maxWarpFactor).toBeCloseTo(9.6);
50+
expect(getState(store).shipName).toEqual('USS Enterprise');
51+
52+
patchState(store, { maxWarpFactor: 9.2 });
53+
tick(1);
54+
expect(getState(store).maxWarpFactor).toBeCloseTo(9.2);
55+
expect(getState(store).shipName).toEqual('USS Enterprise');
56+
57+
// It's just a one-way-sync
58+
expect(maxWarpFactor()).toBeCloseTo(9.6);
59+
});
60+
}));
61+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { computed } from '@angular/core';
2+
import {
3+
EmptyFeatureResult,
4+
patchState,
5+
signalMethod,
6+
SignalStoreFeature,
7+
signalStoreFeature,
8+
SignalStoreFeatureResult,
9+
withMethods,
10+
} from '@ngrx/signals';
11+
12+
export type WithConnectResultType<T extends SignalStoreFeatureResult> =
13+
EmptyFeatureResult & {
14+
methods: { connect(stateFn: () => Partial<T['state']>): void };
15+
};
16+
17+
export function withConnect<
18+
T extends SignalStoreFeatureResult
19+
>(): SignalStoreFeature<T, WithConnectResultType<T>> {
20+
return signalStoreFeature(
21+
withMethods((store) => {
22+
return {
23+
connect(stateFn: () => Partial<T['state']>): void {
24+
const stateSignal = computed(stateFn);
25+
signalMethod<Partial<T['state']>>((state) => {
26+
patchState(store, state);
27+
})(stateSignal);
28+
},
29+
};
30+
})
31+
);
32+
}

0 commit comments

Comments
 (0)