forked from angular-architects/ngrx-toolkit
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
`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) ) ```
- Loading branch information
1 parent
f5039aa
commit bef1528
Showing
10 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { test, expect } from '@playwright/test'; | ||
|
||
test.describe('conditional', () => { | ||
test.beforeEach(async ({ page }) => { | ||
await page.goto(''); | ||
await page.getByRole('link', { name: 'withConditional' }).click(); | ||
}); | ||
|
||
test(`uses real user`, async ({ page }) => { | ||
await page.getByRole('radio', { name: 'Real User' }).click(); | ||
await page.getByRole('button', { name: 'Toggle User Component' }).click(); | ||
|
||
await expect(page.getByText('Current User Konrad')).toBeVisible(); | ||
}); | ||
|
||
test(`uses fake user`, async ({ page }) => { | ||
await page.getByRole('radio', { name: 'Fake User' }).click(); | ||
await page.getByRole('button', { name: 'Toggle User Component' }).click(); | ||
|
||
await expect(page.getByText('Current User Tommy Fake')).toBeVisible(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
apps/demo/src/app/with-conditional/conditional.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { Component, signal, inject, untracked, effect } from '@angular/core'; | ||
import { | ||
patchState, | ||
signalStore, | ||
signalStoreFeature, | ||
withHooks, | ||
withMethods, | ||
withState, | ||
} from '@ngrx/signals'; | ||
import { FormsModule } from '@angular/forms'; | ||
import { | ||
MatButtonToggle, | ||
MatButtonToggleGroup, | ||
} from '@angular/material/button-toggle'; | ||
import { withConditional } from '@angular-architects/ngrx-toolkit'; | ||
import { MatButton } from '@angular/material/button'; | ||
|
||
const withUser = signalStoreFeature( | ||
withState({ id: 0, name: '' }), | ||
withHooks((store) => ({ | ||
onInit() { | ||
patchState(store, { id: 1, name: 'Konrad' }); | ||
}, | ||
})) | ||
); | ||
|
||
const withFakeUser = signalStoreFeature( | ||
withState({ id: 0, name: 'Tommy Fake' }) | ||
); | ||
|
||
const UserServiceStore = signalStore( | ||
{ providedIn: 'root' }, | ||
withState({ implementation: 'real' as 'real' | 'fake' }), | ||
withMethods((store) => ({ | ||
setImplementation(implementation: 'real' | 'fake') { | ||
patchState(store, { implementation }); | ||
}, | ||
})) | ||
); | ||
|
||
const UserStore = signalStore( | ||
withConditional( | ||
() => inject(UserServiceStore).implementation() === 'real', | ||
withUser, | ||
withFakeUser | ||
) | ||
); | ||
|
||
@Component({ | ||
selector: 'demo-conditional-user', | ||
template: `<p>Current User {{ userStore.name() }}</p>`, | ||
providers: [UserStore], | ||
}) | ||
class ConditionalUserComponent { | ||
protected readonly userStore = inject(UserStore); | ||
|
||
constructor() { | ||
console.log('log geht es'); | ||
} | ||
} | ||
|
||
@Component({ | ||
template: ` | ||
<h2> | ||
<pre>withConditional</pre> | ||
</h2> | ||
<mat-button-toggle-group | ||
aria-label="User Feature" | ||
[(ngModel)]="userFeature" | ||
> | ||
<mat-button-toggle value="real">Real User</mat-button-toggle> | ||
<mat-button-toggle value="fake">Fake User</mat-button-toggle> | ||
</mat-button-toggle-group> | ||
<div> | ||
<button mat-raised-button (click)="toggleUserComponent()"> | ||
Toggle User Component | ||
</button> | ||
</div> | ||
@if (showUserComponent()) { | ||
<demo-conditional-user /> | ||
} | ||
`, | ||
imports: [ | ||
FormsModule, | ||
MatButtonToggle, | ||
MatButtonToggleGroup, | ||
ConditionalUserComponent, | ||
MatButton, | ||
], | ||
}) | ||
export class ConditionalSettingComponent { | ||
showUserComponent = signal(false); | ||
|
||
toggleUserComponent() { | ||
this.showUserComponent.update((show) => !show); | ||
} | ||
userService = inject(UserServiceStore); | ||
protected readonly userFeature = signal<'real' | 'fake'>('real'); | ||
|
||
effRef = effect(() => { | ||
const userFeature = this.userFeature(); | ||
|
||
untracked(() => { | ||
this.userService.setImplementation(userFeature); | ||
this.showUserComponent.set(false); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
--- | ||
title: 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) | ||
); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { | ||
getState, | ||
patchState, | ||
signalStore, | ||
signalStoreFeature, | ||
withHooks, | ||
withMethods, | ||
withState, | ||
} from '@ngrx/signals'; | ||
import { emptyFeature, withConditional } from './with-conditional'; | ||
import { inject, InjectionToken } from '@angular/core'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { withDevtools } from './devtools/with-devtools'; | ||
|
||
describe('withConditional', () => { | ||
const withUser = signalStoreFeature( | ||
withState({ id: 0, name: '' }), | ||
withHooks((store) => ({ | ||
onInit() { | ||
patchState(store, { id: 1, name: 'Konrad' }); | ||
}, | ||
})) | ||
); | ||
|
||
const withFakeUser = signalStoreFeature( | ||
withState({ id: 0, name: 'Tommy Fake' }) | ||
); | ||
|
||
for (const isReal of [true, false]) { | ||
it(`should ${isReal ? '' : 'not '} enable withUser`, () => { | ||
const REAL_USER_TOKEN = new InjectionToken('REAL_USER', { | ||
providedIn: 'root', | ||
factory: () => isReal, | ||
}); | ||
const UserStore = signalStore( | ||
{ providedIn: 'root' }, | ||
withConditional(() => inject(REAL_USER_TOKEN), withUser, withFakeUser) | ||
); | ||
const userStore = TestBed.inject(UserStore); | ||
|
||
if (isReal) { | ||
expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' }); | ||
} else { | ||
expect(getState(userStore)).toEqual({ id: 0, name: 'Tommy Fake' }); | ||
} | ||
}); | ||
} | ||
|
||
it(`should access the store`, () => { | ||
const UserStore = signalStore( | ||
{ providedIn: 'root' }, | ||
withMethods(() => ({ | ||
useRealUser: () => true, | ||
})), | ||
withConditional((store) => store.useRealUser(), withUser, withFakeUser) | ||
); | ||
const userStore = TestBed.inject(UserStore); | ||
|
||
expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' }); | ||
}); | ||
|
||
it('should be used inside a signalStoreFeature', () => { | ||
const withConditionalUser = (activate: boolean) => | ||
signalStoreFeature( | ||
withConditional(() => activate, withUser, withFakeUser) | ||
); | ||
|
||
const UserStore = signalStore( | ||
{ providedIn: 'root' }, | ||
withConditionalUser(true) | ||
); | ||
const userStore = TestBed.inject(UserStore); | ||
|
||
expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' }); | ||
}); | ||
|
||
it('should ensure that both features return the same type', () => { | ||
const withUser = signalStoreFeature( | ||
withState({ id: 0, name: '' }), | ||
withHooks((store) => ({ | ||
onInit() { | ||
patchState(store, { id: 1, name: 'Konrad' }); | ||
}, | ||
})) | ||
); | ||
|
||
const withFakeUser = signalStoreFeature( | ||
withState({ id: 0, firstname: 'Tommy Fake' }) | ||
); | ||
|
||
// @ts-expect-error withFakeUser has a different state shape | ||
signalStore(withConditional(() => true, withUser, withFakeUser)); | ||
}); | ||
|
||
it('should also work with empty features', () => { | ||
signalStore( | ||
withConditional( | ||
() => true, | ||
withDevtools('dummy'), | ||
signalStoreFeature(withState({})) | ||
) | ||
); | ||
}); | ||
|
||
it('should work with `emptyFeature` if falsy is skipped', () => { | ||
signalStore( | ||
withConditional( | ||
() => true, | ||
signalStoreFeature(withState({})), | ||
emptyFeature | ||
) | ||
); | ||
}); | ||
|
||
it('should not work with `emptyFeature` if feature is not empty', () => { | ||
signalStore( | ||
withConditional( | ||
() => true, | ||
// @ts-expect-error feature is not empty | ||
() => signalStoreFeature(withState({ x: 1 })), | ||
emptyFeature | ||
) | ||
); | ||
}); | ||
}); |
Oops, something went wrong.