Skip to content

Commit bef1528

Browse files
feat: add withConditional()
`withConditional` activates a feature based on a given condition. ## Use Cases - Conditionally activate features based on the **store state** or other criteria. - Choose between **two different implementations** of a feature. ## Type Constraints Both features must have **exactly the same state, props, and methods**. Otherwise, a type error will occur. ## Usage ```typescript const withUser = signalStoreFeature( withState({ id: 1, name: 'Konrad' }), withHooks(store => ({ onInit() { // user loading logic } })) ); function withFakeUser() { return signalStoreFeature( withState({ id: 0, name: 'anonymous' }) ); } signalStore( withMethods(() => ({ useRealUser: () => true })), withConditional((store) => store.useRealUser(), withUser, withFakeUser) ) ```
1 parent f5039aa commit bef1528

File tree

10 files changed

+381
-0
lines changed

10 files changed

+381
-0
lines changed

apps/demo/e2e/conditional.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('conditional', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('');
6+
await page.getByRole('link', { name: 'withConditional' }).click();
7+
});
8+
9+
test(`uses real user`, async ({ page }) => {
10+
await page.getByRole('radio', { name: 'Real User' }).click();
11+
await page.getByRole('button', { name: 'Toggle User Component' }).click();
12+
13+
await expect(page.getByText('Current User Konrad')).toBeVisible();
14+
});
15+
16+
test(`uses fake user`, async ({ page }) => {
17+
await page.getByRole('radio', { name: 'Fake User' }).click();
18+
await page.getByRole('button', { name: 'Toggle User Component' }).click();
19+
20+
await expect(page.getByText('Current User Tommy Fake')).toBeVisible();
21+
});
22+
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<a mat-list-item routerLink="/reset">withReset</a>
2323
<a mat-list-item routerLink="/immutable-state">withImmutableState</a>
2424
<a mat-list-item routerLink="/feature-factory">withFeatureFactory</a>
25+
<a mat-list-item routerLink="/conditional">withConditional</a>
2526
</mat-nav-list>
2627
</mat-drawer>
2728
<mat-drawer-content>

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,11 @@ export const lazyRoutes: Route[] = [
5252
(m) => m.FeatureFactoryComponent
5353
),
5454
},
55+
{
56+
path: 'conditional',
57+
loadComponent: () =>
58+
import('./with-conditional/conditional.component').then(
59+
(m) => m.ConditionalSettingComponent
60+
),
61+
},
5562
];
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Component, signal, inject, untracked, effect } from '@angular/core';
2+
import {
3+
patchState,
4+
signalStore,
5+
signalStoreFeature,
6+
withHooks,
7+
withMethods,
8+
withState,
9+
} from '@ngrx/signals';
10+
import { FormsModule } from '@angular/forms';
11+
import {
12+
MatButtonToggle,
13+
MatButtonToggleGroup,
14+
} from '@angular/material/button-toggle';
15+
import { withConditional } from '@angular-architects/ngrx-toolkit';
16+
import { MatButton } from '@angular/material/button';
17+
18+
const withUser = signalStoreFeature(
19+
withState({ id: 0, name: '' }),
20+
withHooks((store) => ({
21+
onInit() {
22+
patchState(store, { id: 1, name: 'Konrad' });
23+
},
24+
}))
25+
);
26+
27+
const withFakeUser = signalStoreFeature(
28+
withState({ id: 0, name: 'Tommy Fake' })
29+
);
30+
31+
const UserServiceStore = signalStore(
32+
{ providedIn: 'root' },
33+
withState({ implementation: 'real' as 'real' | 'fake' }),
34+
withMethods((store) => ({
35+
setImplementation(implementation: 'real' | 'fake') {
36+
patchState(store, { implementation });
37+
},
38+
}))
39+
);
40+
41+
const UserStore = signalStore(
42+
withConditional(
43+
() => inject(UserServiceStore).implementation() === 'real',
44+
withUser,
45+
withFakeUser
46+
)
47+
);
48+
49+
@Component({
50+
selector: 'demo-conditional-user',
51+
template: `<p>Current User {{ userStore.name() }}</p>`,
52+
providers: [UserStore],
53+
})
54+
class ConditionalUserComponent {
55+
protected readonly userStore = inject(UserStore);
56+
57+
constructor() {
58+
console.log('log geht es');
59+
}
60+
}
61+
62+
@Component({
63+
template: `
64+
<h2>
65+
<pre>withConditional</pre>
66+
</h2>
67+
68+
<mat-button-toggle-group
69+
aria-label="User Feature"
70+
[(ngModel)]="userFeature"
71+
>
72+
<mat-button-toggle value="real">Real User</mat-button-toggle>
73+
<mat-button-toggle value="fake">Fake User</mat-button-toggle>
74+
</mat-button-toggle-group>
75+
76+
<div>
77+
<button mat-raised-button (click)="toggleUserComponent()">
78+
Toggle User Component
79+
</button>
80+
</div>
81+
@if (showUserComponent()) {
82+
<demo-conditional-user />
83+
}
84+
`,
85+
imports: [
86+
FormsModule,
87+
MatButtonToggle,
88+
MatButtonToggleGroup,
89+
ConditionalUserComponent,
90+
MatButton,
91+
],
92+
})
93+
export class ConditionalSettingComponent {
94+
showUserComponent = signal(false);
95+
96+
toggleUserComponent() {
97+
this.showUserComponent.update((show) => !show);
98+
}
99+
userService = inject(UserServiceStore);
100+
protected readonly userFeature = signal<'real' | 'fake'>('real');
101+
102+
effRef = effect(() => {
103+
const userFeature = this.userFeature();
104+
105+
untracked(() => {
106+
this.userService.setImplementation(userFeature);
107+
this.showUserComponent.set(false);
108+
});
109+
});
110+
}

docs/docs/extensions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The NgRx Toolkit is a set of extensions to the NgRx SignalsStore.
77
It offers extensions like:
88

99
- [⭐️ Devtools](./with-devtools): Integration into Redux Devtools
10+
- [Conditional Features](./with-conditional): Allows adding features to the store conditionally
1011
- [DataService](./with-data-service): Builds on top of `withEntities` and adds the backend synchronization to it
1112
- [Feature Factory](./with-feature-factory): Allows passing properties, methods, or signals from a SignalStore to a custom feature (`signalStoreFeature`).
1213
- [Immutable State Protection](./with-immutable-state): Protects the state from being mutated outside or inside the Store.

docs/docs/with-conditional.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: withConditional()
3+
---
4+
5+
`withConditional` activates a feature based on a given condition.
6+
7+
## Use Cases
8+
9+
- Conditionally activate features based on the **store state** or other criteria.
10+
- Choose between **two different implementations** of a feature.
11+
12+
## Type Constraints
13+
14+
Both features must have **exactly the same state, props, and methods**.
15+
Otherwise, a type error will occur.
16+
17+
## Usage
18+
19+
```typescript
20+
const withUser = signalStoreFeature(
21+
withState({ id: 1, name: 'Konrad' }),
22+
withHooks((store) => ({
23+
onInit() {
24+
// user loading logic
25+
},
26+
}))
27+
);
28+
29+
function withFakeUser() {
30+
return signalStoreFeature(withState({ id: 0, name: 'anonymous' }));
31+
}
32+
33+
signalStore(
34+
withMethods(() => ({
35+
useRealUser: () => true,
36+
})),
37+
withConditional((store) => store.useRealUser(), withUser, withFakeUser)
38+
);
39+
```

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const sidebars: SidebarsConfig = {
2323
'with-undo-redo',
2424
'with-immutable-state',
2525
'with-feature-factory',
26+
'with-conditional',
2627
],
2728
reduxConnectorSidebar: [
2829
{

libs/ngrx-toolkit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export * from './lib/with-pagination';
2222
export { withReset, setResetState } from './lib/with-reset';
2323
export { withImmutableState } from './lib/immutable-state/with-immutable-state';
2424
export { withFeatureFactory } from './lib/with-feature-factory';
25+
export { withConditional, emptyFeature } from './lib/with-conditional';
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
getState,
3+
patchState,
4+
signalStore,
5+
signalStoreFeature,
6+
withHooks,
7+
withMethods,
8+
withState,
9+
} from '@ngrx/signals';
10+
import { emptyFeature, withConditional } from './with-conditional';
11+
import { inject, InjectionToken } from '@angular/core';
12+
import { TestBed } from '@angular/core/testing';
13+
import { withDevtools } from './devtools/with-devtools';
14+
15+
describe('withConditional', () => {
16+
const withUser = signalStoreFeature(
17+
withState({ id: 0, name: '' }),
18+
withHooks((store) => ({
19+
onInit() {
20+
patchState(store, { id: 1, name: 'Konrad' });
21+
},
22+
}))
23+
);
24+
25+
const withFakeUser = signalStoreFeature(
26+
withState({ id: 0, name: 'Tommy Fake' })
27+
);
28+
29+
for (const isReal of [true, false]) {
30+
it(`should ${isReal ? '' : 'not '} enable withUser`, () => {
31+
const REAL_USER_TOKEN = new InjectionToken('REAL_USER', {
32+
providedIn: 'root',
33+
factory: () => isReal,
34+
});
35+
const UserStore = signalStore(
36+
{ providedIn: 'root' },
37+
withConditional(() => inject(REAL_USER_TOKEN), withUser, withFakeUser)
38+
);
39+
const userStore = TestBed.inject(UserStore);
40+
41+
if (isReal) {
42+
expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' });
43+
} else {
44+
expect(getState(userStore)).toEqual({ id: 0, name: 'Tommy Fake' });
45+
}
46+
});
47+
}
48+
49+
it(`should access the store`, () => {
50+
const UserStore = signalStore(
51+
{ providedIn: 'root' },
52+
withMethods(() => ({
53+
useRealUser: () => true,
54+
})),
55+
withConditional((store) => store.useRealUser(), withUser, withFakeUser)
56+
);
57+
const userStore = TestBed.inject(UserStore);
58+
59+
expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' });
60+
});
61+
62+
it('should be used inside a signalStoreFeature', () => {
63+
const withConditionalUser = (activate: boolean) =>
64+
signalStoreFeature(
65+
withConditional(() => activate, withUser, withFakeUser)
66+
);
67+
68+
const UserStore = signalStore(
69+
{ providedIn: 'root' },
70+
withConditionalUser(true)
71+
);
72+
const userStore = TestBed.inject(UserStore);
73+
74+
expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' });
75+
});
76+
77+
it('should ensure that both features return the same type', () => {
78+
const withUser = signalStoreFeature(
79+
withState({ id: 0, name: '' }),
80+
withHooks((store) => ({
81+
onInit() {
82+
patchState(store, { id: 1, name: 'Konrad' });
83+
},
84+
}))
85+
);
86+
87+
const withFakeUser = signalStoreFeature(
88+
withState({ id: 0, firstname: 'Tommy Fake' })
89+
);
90+
91+
// @ts-expect-error withFakeUser has a different state shape
92+
signalStore(withConditional(() => true, withUser, withFakeUser));
93+
});
94+
95+
it('should also work with empty features', () => {
96+
signalStore(
97+
withConditional(
98+
() => true,
99+
withDevtools('dummy'),
100+
signalStoreFeature(withState({}))
101+
)
102+
);
103+
});
104+
105+
it('should work with `emptyFeature` if falsy is skipped', () => {
106+
signalStore(
107+
withConditional(
108+
() => true,
109+
signalStoreFeature(withState({})),
110+
emptyFeature
111+
)
112+
);
113+
});
114+
115+
it('should not work with `emptyFeature` if feature is not empty', () => {
116+
signalStore(
117+
withConditional(
118+
() => true,
119+
// @ts-expect-error feature is not empty
120+
() => signalStoreFeature(withState({ x: 1 })),
121+
emptyFeature
122+
)
123+
);
124+
});
125+
});

0 commit comments

Comments
 (0)