-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(signals): add
withReset()
feature to SignalStore
`withReset()` adds a `resetState` method to reset the state of a SignalStore instance. By default, the initial state will be the reset state. The `setResetState` method allows customizing the reset state. - Unit and E2E tests added - Demo app updated with an example - Documentation updated
- Loading branch information
1 parent
f3bad94
commit 90a64b3
Showing
10 changed files
with
417 additions
and
2 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,30 @@ | ||
import { test, expect } from '@playwright/test'; | ||
|
||
test('has title', async ({ page }) => { | ||
await page.goto(''); | ||
await page.getByRole('link', { name: 'reset' }).click(); | ||
await page | ||
.getByRole('row', { name: 'Go for a walk' }) | ||
.getByRole('checkbox') | ||
.click(); | ||
await page | ||
.getByRole('row', { name: 'Exercise' }) | ||
.getByRole('checkbox') | ||
.click(); | ||
|
||
await expect( | ||
page.getByRole('row', { name: 'Go for a walk' }).getByRole('checkbox') | ||
).toBeChecked(); | ||
await expect( | ||
page.getByRole('row', { name: 'Exercise' }).getByRole('checkbox') | ||
).toBeChecked(); | ||
|
||
await page.getByRole('button', { name: 'Reset State' }).click(); | ||
|
||
await expect( | ||
page.getByRole('row', { name: 'Go for a walk' }).getByRole('checkbox') | ||
).not.toBeChecked(); | ||
await expect( | ||
page.getByRole('row', { name: 'Exercise' }).getByRole('checkbox') | ||
).not.toBeChecked(); | ||
}); |
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,84 @@ | ||
import { | ||
getState, | ||
patchState, | ||
signalStore, | ||
withHooks, | ||
withMethods, | ||
withState, | ||
} from '@ngrx/signals'; | ||
import { addEntity, updateEntity, withEntities } from '@ngrx/signals/entities'; | ||
import { setResetState, withReset } from '@angular-architects/ngrx-toolkit'; | ||
|
||
export interface Todo { | ||
id: number; | ||
name: string; | ||
finished: boolean; | ||
description?: string; | ||
deadline?: Date; | ||
} | ||
|
||
export type AddTodo = Omit<Todo, 'id'>; | ||
|
||
export const TodoStore = signalStore( | ||
{ providedIn: 'root' }, | ||
withReset(), | ||
withEntities<Todo>(), | ||
withState({ | ||
selectedIds: [] as number[], | ||
}), | ||
withMethods((store) => { | ||
let currentId = 0; | ||
return { | ||
_add(todo: AddTodo) { | ||
patchState(store, addEntity({ ...todo, id: ++currentId })); | ||
}, | ||
toggleFinished(id: number) { | ||
const todo = store.entityMap()[id]; | ||
patchState( | ||
store, | ||
updateEntity({ id, changes: { finished: !todo.finished } }) | ||
); | ||
}, | ||
}; | ||
}), | ||
withHooks({ | ||
onInit: (store) => { | ||
store._add({ | ||
name: 'Go for a Walk', | ||
finished: false, | ||
description: | ||
'Go for a walk in the park to relax and enjoy nature. Walking is a great way to clear your mind and get some exercise. It can help reduce stress and improve your mood. Make sure to wear comfortable shoes and bring a bottle of water. Enjoy the fresh air and take in the scenery around you.', | ||
}); | ||
|
||
store._add({ | ||
name: 'Read a Book', | ||
finished: false, | ||
description: | ||
'Spend some time reading a book. It can be a novel, a non-fiction book, or any other genre you enjoy. Reading can help you relax and learn new things.', | ||
}); | ||
|
||
store._add({ | ||
name: 'Write a Journal', | ||
finished: false, | ||
description: | ||
'Take some time to write in your journal. Reflect on your day, your thoughts, and your feelings. Journaling can be a great way to process emotions and document your life.', | ||
}); | ||
|
||
store._add({ | ||
name: 'Exercise', | ||
finished: false, | ||
description: | ||
'Do some physical exercise. It can be a workout, a run, or any other form of exercise you enjoy. Exercise is important for maintaining physical and mental health.', | ||
}); | ||
|
||
store._add({ | ||
name: 'Cook a Meal', | ||
finished: false, | ||
description: | ||
'Prepare a meal for yourself or your family. Cooking can be a fun and rewarding activity. Try out a new recipe or make one of your favorite dishes.', | ||
}); | ||
|
||
setResetState(store, getState(store)); | ||
}, | ||
}) | ||
); |
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,70 @@ | ||
import { Component, effect, inject } from '@angular/core'; | ||
import { MatCheckboxModule } from '@angular/material/checkbox'; | ||
import { MatIconModule } from '@angular/material/icon'; | ||
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; | ||
import { SelectionModel } from '@angular/cdk/collections'; | ||
import { Todo, TodoStore } from './todo-store'; | ||
import { MatButton } from '@angular/material/button'; | ||
|
||
@Component({ | ||
template: ` | ||
<div class="button"> | ||
<button mat-raised-button (click)="resetState()">Reset State</button> | ||
</div> | ||
<div> | ||
<mat-table [dataSource]="dataSource" class="mat-elevation-z8"> | ||
<!-- Checkbox Column --> | ||
<ng-container matColumnDef="finished"> | ||
<mat-header-cell *matHeaderCellDef></mat-header-cell> | ||
<mat-cell *matCellDef="let row" class="actions"> | ||
<mat-checkbox | ||
(click)="$event.stopPropagation()" | ||
(change)="toggleFinished(row)" | ||
[checked]="row.finished" | ||
> | ||
</mat-checkbox> | ||
</mat-cell> | ||
</ng-container> | ||
<!-- Name Column --> | ||
<ng-container matColumnDef="name"> | ||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell> | ||
<mat-cell *matCellDef="let element">{{ element.name }}</mat-cell> | ||
</ng-container> | ||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> | ||
<mat-row | ||
*matRowDef="let row; columns: displayedColumns" | ||
(click)="selection.toggle(row)" | ||
></mat-row> | ||
</mat-table> | ||
</div> | ||
`, | ||
styles: `.button { | ||
margin-bottom: 1em; | ||
} | ||
`, | ||
imports: [MatCheckboxModule, MatIconModule, MatTableModule, MatButton], | ||
}) | ||
export class TodoComponent { | ||
todoStore = inject(TodoStore); | ||
|
||
displayedColumns: string[] = ['finished', 'name']; | ||
dataSource = new MatTableDataSource<Todo>([]); | ||
selection = new SelectionModel<Todo>(true, []); | ||
|
||
constructor() { | ||
effect(() => { | ||
this.dataSource.data = this.todoStore.entities(); | ||
}); | ||
} | ||
|
||
toggleFinished(todo: Todo) { | ||
this.todoStore.toggleFinished(todo.id); | ||
} | ||
|
||
resetState() { | ||
this.todoStore.resetState(); | ||
} | ||
} |
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,49 @@ | ||
--- | ||
title: withReset() | ||
--- | ||
|
||
`withReset()` adds a method the state of the Signal Store to its initial value. Nothing more to say about it 😅 | ||
|
||
Example: | ||
|
||
```typescript | ||
const Store = signalStore( | ||
withState({ | ||
user: { id: 1, name: 'Konrad' }, | ||
address: { city: 'Vienna', zip: '1010' }, | ||
}), | ||
withReset(), // <-- the reset extension | ||
withMethods((store) => ({ | ||
changeUser(id: number, name: string) { | ||
patchState(store, { user: { id, name } }); | ||
}, | ||
changeUserName(name: string) { | ||
patchState(store, (value) => ({ user: { ...value.user, name } })); | ||
}, | ||
})) | ||
); | ||
|
||
const store = new Store(); | ||
|
||
store.changeUser(2, 'John'); | ||
console.log(store.user()); // { id: 2, name: 'John' } | ||
|
||
store.resetState(); | ||
console.log(store.user()); // { id: 1, name: 'Konrad' } | ||
``` | ||
|
||
## `setResetState()` | ||
|
||
If you want to set a custom reset state, you can use the `setResetState()` method. | ||
|
||
Example: | ||
|
||
```typescript | ||
// continue from the previous example | ||
|
||
setResetState(store, { user: { id: 3, name: 'Jane' }, address: { city: 'Berlin', zip: '10115' } }); | ||
store.changeUser(4, 'Alice'); | ||
|
||
store.resetState(); | ||
console.log(store.user()); // { id: 3, name: 'Jane' } | ||
``` |
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,112 @@ | ||
import { | ||
getState, | ||
patchState, | ||
signalStore, | ||
withMethods, | ||
withState, | ||
} from '@ngrx/signals'; | ||
import { setResetState, withReset } from './with-reset'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { effect } from '@angular/core'; | ||
|
||
describe('withReset', () => { | ||
const setup = () => { | ||
const initialState = { | ||
user: { id: 1, name: 'Konrad' }, | ||
address: { city: 'Vienna', zip: '1010' }, | ||
}; | ||
|
||
const Store = signalStore( | ||
withState(initialState), | ||
withReset(), | ||
withMethods((store) => ({ | ||
changeUser(id: number, name: string) { | ||
patchState(store, { user: { id, name } }); | ||
}, | ||
changeUserName(name: string) { | ||
patchState(store, (value) => ({ user: { ...value.user, name } })); | ||
}, | ||
changeAddress(city: string, zip: string) { | ||
patchState(store, { address: { city, zip } }); | ||
}, | ||
})) | ||
); | ||
|
||
const store = TestBed.configureTestingModule({ | ||
providers: [Store], | ||
}).inject(Store); | ||
|
||
return { store, initialState }; | ||
}; | ||
|
||
it('should reset state to initial state', () => { | ||
const { store, initialState } = setup(); | ||
|
||
store.changeUser(2, 'Max'); | ||
expect(getState(store)).toMatchObject({ | ||
user: { id: 2, name: 'Max' }, | ||
}); | ||
store.resetState(); | ||
expect(getState(store)).toStrictEqual(initialState); | ||
}); | ||
|
||
it('should not fire if reset is called on unchanged state', () => { | ||
const { store } = setup(); | ||
let effectCounter = 0; | ||
TestBed.runInInjectionContext(() => { | ||
effect(() => { | ||
store.user(); | ||
effectCounter++; | ||
}); | ||
}); | ||
TestBed.flushEffects(); | ||
store.resetState(); | ||
TestBed.flushEffects(); | ||
expect(effectCounter).toBe(1); | ||
}); | ||
|
||
it('should not fire on props which are unchanged', () => { | ||
const { store } = setup(); | ||
let effectCounter = 0; | ||
TestBed.runInInjectionContext(() => { | ||
effect(() => { | ||
store.address(); | ||
effectCounter++; | ||
}); | ||
}); | ||
|
||
TestBed.flushEffects(); | ||
expect(effectCounter).toBe(1); | ||
store.changeUserName('Max'); | ||
TestBed.flushEffects(); | ||
store.changeUser(2, 'Ludwig'); | ||
TestBed.flushEffects(); | ||
expect(effectCounter).toBe(1); | ||
}); | ||
|
||
it('should be possible to change the reset state', () => { | ||
const { store } = setup(); | ||
|
||
setResetState(store, { | ||
user: { id: 2, name: 'Max' }, | ||
address: { city: 'London', zip: 'SW1' }, | ||
}); | ||
|
||
store.changeUser(3, 'Ludwig'); | ||
store.changeAddress('Paris', '75001'); | ||
|
||
store.resetState(); | ||
expect(getState(store)).toEqual({ | ||
user: { id: 2, name: 'Max' }, | ||
address: { city: 'London', zip: 'SW1' }, | ||
}); | ||
}); | ||
|
||
it('should throw on setResetState if store is not configured with withReset()', () => { | ||
const Store = signalStore({ providedIn: 'root' }, withState({})); | ||
const store = TestBed.inject(Store); | ||
expect(() => setResetState(store, {})).toThrowError( | ||
'Cannot set reset state, since store is not configured with withReset()' | ||
); | ||
}); | ||
}); |
Oops, something went wrong.