Skip to content

Commit 6a03c77

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 6a03c77

File tree

8 files changed

+194
-0
lines changed

8 files changed

+194
-0
lines changed

apps/demo/src/app/app.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<a mat-list-item routerLink="/immutable-state">withImmutableState</a>
2727
<a mat-list-item routerLink="/feature-factory">withFeatureFactory</a>
2828
<a mat-list-item routerLink="/conditional">withConditional</a>
29+
<a mat-list-item routerLink="/connect">withConnect</a>
2930
</mat-nav-list>
3031
</mat-drawer>
3132
<mat-drawer-content>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Component, inject, linkedSignal } from '@angular/core';
2+
import { signalStore, withState } from '@ngrx/signals';
3+
import { FormsModule } from '@angular/forms';
4+
import { withConnect } from '@angular-architects/ngrx-toolkit';
5+
6+
const initialState = { user: { name: 'Max' } };
7+
8+
const UserStore = signalStore(
9+
{ providedIn: 'root' },
10+
withState(initialState),
11+
withConnect()
12+
);
13+
14+
@Component({
15+
template: `
16+
<h2>
17+
<pre>withConnect</pre>
18+
</h2>
19+
<p>
20+
withConnect() adds a method to connect Signals back to your store.
21+
Everytime these Signals change, the store will be updated accordingly.
22+
</p>
23+
24+
<p>User name in Store: {{ userStore.user.name() }}</p>
25+
26+
<p>Connected local user name: <input [(ngModel)]="userName" /></p>
27+
`,
28+
imports: [FormsModule],
29+
})
30+
export class ConnectComponent {
31+
protected readonly userStore = inject(UserStore);
32+
protected readonly userName = linkedSignal(() => this.userStore.user.name());
33+
34+
constructor() {
35+
this.userStore.connect(() => ({
36+
user: {
37+
name: this.userName(),
38+
},
39+
}));
40+
}
41+
}

apps/demo/src/app/lazy-routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,9 @@ export const lazyRoutes: Route[] = [
6161
(m) => m.ConditionalSettingComponent
6262
),
6363
},
64+
{
65+
path: 'connect',
66+
loadComponent: () =>
67+
import('./connect/connect.component').then((m) => m.ConnectComponent),
68+
},
6469
];

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)