Skip to content

Commit c5fa930

Browse files
feat(redux): introduce withRedux extension
1 parent 0fa5e67 commit c5fa930

16 files changed

+532
-96
lines changed

apps/demo/.eslintrc.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
"error",
1414
{
1515
"type": "attribute",
16-
"prefix": "ngrxToolkit",
16+
"prefix": "demo",
1717
"style": "camelCase"
1818
}
1919
],
2020
"@angular-eslint/component-selector": [
2121
"error",
2222
{
2323
"type": "element",
24-
"prefix": "ngrx-toolkit",
24+
"prefix": "demo",
2525
"style": "kebab-case"
2626
}
2727
]

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

+5-42
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,6 @@
1-
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
2-
<!-- Checkbox Column -->
3-
<ng-container matColumnDef="finished">
4-
<mat-header-cell *matHeaderCellDef></mat-header-cell>
5-
<mat-cell *matCellDef="let row" class="actions">
6-
<mat-checkbox
7-
(click)="$event.stopPropagation()"
8-
(change)="checkboxLabel(row)"
9-
[checked]="row.finished"
10-
>
11-
</mat-checkbox>
12-
<mat-icon (click)="removeTodo(row)">delete</mat-icon>
13-
</mat-cell>
14-
</ng-container>
1+
<ul>
2+
<li><a routerLink="/todo">Todo - DevTools Showcase</a></li>
3+
<li><a routerLink="/flight-search">Flight Search - withRedux Showcase</a></li>
4+
</ul>
155

16-
<!-- Name Column -->
17-
<ng-container matColumnDef="name">
18-
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
19-
<mat-cell *matCellDef="let element">{{ element.name }}</mat-cell>
20-
</ng-container>
21-
22-
<!-- Description Column -->
23-
<ng-container matColumnDef="description">
24-
<mat-header-cell *matHeaderCellDef>Description</mat-header-cell>
25-
<mat-cell *matCellDef="let element">{{ element.description }}</mat-cell>
26-
</ng-container>
27-
28-
<!-- Deadline Column -->
29-
<ng-container matColumnDef="deadline">
30-
<mat-header-cell mat-header-cell *matHeaderCellDef
31-
>Deadline</mat-header-cell
32-
>
33-
<mat-cell mat-cell *matCellDef="let element">{{
34-
element.deadline
35-
}}</mat-cell>
36-
</ng-container>
37-
38-
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
39-
<mat-row
40-
*matRowDef="let row; columns: displayedColumns"
41-
(click)="selection.toggle(row)"
42-
></mat-row>
43-
</mat-table>
6+
<router-outlet />

apps/demo/src/app/app.component.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ import { Component, effect, inject } from '@angular/core';
33
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
44
import { MatCheckboxModule } from '@angular/material/checkbox';
55
import { Todo, TodoStore } from './todo-store';
6-
import { JsonPipe } from '@angular/common';
76
import { MatIconModule } from '@angular/material/icon';
87
import { CategoryStore } from './category.store';
8+
import { RouterLink, RouterOutlet } from '@angular/router';
99

1010
@Component({
1111
selector: 'demo-root',
1212
templateUrl: './app.component.html',
1313
standalone: true,
14-
imports: [MatTableModule, MatCheckboxModule, MatIconModule],
14+
imports: [
15+
MatTableModule,
16+
MatCheckboxModule,
17+
MatIconModule,
18+
RouterLink,
19+
RouterOutlet,
20+
],
1521
styleUrl: './app.component.css',
1622
})
1723
export class AppComponent {

apps/demo/src/app/app.config.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { provideRouter } from '@angular/router';
33
import { appRoutes } from './app.routes';
44
import { provideClientHydration } from '@angular/platform-browser';
55
import { provideAnimations } from '@angular/platform-browser/animations';
6+
import { provideHttpClient } from '@angular/common/http';
67

78
export const appConfig: ApplicationConfig = {
8-
providers: [provideClientHydration(), provideRouter(appRoutes), provideAnimations()],
9+
providers: [
10+
provideClientHydration(),
11+
provideRouter(appRoutes),
12+
provideAnimations(),
13+
provideHttpClient(),
14+
],
915
};

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
import { Route } from '@angular/router';
2+
import { TodoComponent } from './todo/todo.component';
3+
import { FlightSearchComponent } from './flight-search/flight-search.component';
24

3-
export const appRoutes: Route[] = [];
5+
export const appRoutes: Route[] = [
6+
{ path: 'todo', component: TodoComponent },
7+
{ path: 'flight-search', component: FlightSearchComponent },
8+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<form (ngSubmit)="search()">
2+
<div>
3+
<mat-form-field>
4+
<mat-label>Name</mat-label>
5+
<input [(ngModel)]="searchParams.from" name="from" matInput />
6+
</mat-form-field>
7+
</div>
8+
9+
<div>
10+
<mat-form-field>
11+
<mat-label>Name</mat-label>
12+
<input [(ngModel)]="searchParams.to" name="to" matInput />
13+
</mat-form-field>
14+
</div>
15+
16+
<button mat-raised-button>Search</button>
17+
</form>
18+
19+
<mat-table [dataSource]="dataSource">
20+
<!-- From Column -->
21+
<ng-container matColumnDef="from">
22+
<mat-header-cell *matHeaderCellDef>From</mat-header-cell>
23+
<mat-cell *matCellDef="let element">{{ element.from }}</mat-cell>
24+
</ng-container>
25+
26+
<!-- To Column -->
27+
<ng-container matColumnDef="to">
28+
<mat-header-cell *matHeaderCellDef>To</mat-header-cell>
29+
<mat-cell *matCellDef="let element">{{ element.to }}</mat-cell>
30+
</ng-container>
31+
32+
<!-- Date Column -->
33+
<ng-container matColumnDef="date">
34+
<mat-header-cell mat-header-cell *matHeaderCellDef>Date</mat-header-cell>
35+
<mat-cell mat-cell *matCellDef="let element">{{
36+
element.date | date
37+
}}</mat-cell>
38+
</ng-container>
39+
40+
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
41+
<mat-row
42+
*matRowDef="let row; columns: displayedColumns"
43+
(click)="selection.toggle(row)"
44+
></mat-row>
45+
</mat-table>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Component, effect, inject } from '@angular/core';
2+
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
3+
import { DatePipe } from '@angular/common';
4+
import { SelectionModel } from '@angular/cdk/collections';
5+
import { Flight } from './flight';
6+
import { FlightStore } from './flight-store';
7+
import { MatInputModule } from '@angular/material/input';
8+
import { FormsModule } from '@angular/forms';
9+
import { MatButtonModule } from '@angular/material/button';
10+
11+
@Component({
12+
selector: 'demo-flight-search',
13+
templateUrl: 'flight-search.component.html',
14+
standalone: true,
15+
imports: [
16+
MatTableModule,
17+
DatePipe,
18+
MatInputModule,
19+
FormsModule,
20+
MatButtonModule,
21+
],
22+
})
23+
export class FlightSearchComponent {
24+
searchParams: { from: string; to: string } = { from: 'Paris', to: 'London' };
25+
flightStore = inject(FlightStore);
26+
27+
displayedColumns: string[] = ['from', 'to', 'date'];
28+
dataSource = new MatTableDataSource<Flight>([]);
29+
selection = new SelectionModel<Flight>(true, []);
30+
31+
constructor() {
32+
effect(() => {
33+
this.dataSource.data = this.flightStore.flights();
34+
});
35+
}
36+
37+
search() {
38+
this.flightStore.loadFlights(this.searchParams);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { signalStore, withState } from '@ngrx/signals';
2+
import {
3+
noPayload,
4+
payload,
5+
withDevtools,
6+
withRedux,
7+
patchState,
8+
} from 'ngrx-toolkit';
9+
import { inject } from '@angular/core';
10+
import { HttpClient, HttpParams } from '@angular/common/http';
11+
import { map, switchMap } from 'rxjs';
12+
import { Flight } from './flight';
13+
14+
export const FlightStore = signalStore(
15+
{ providedIn: 'root' },
16+
withDevtools('flights'),
17+
withState({ flights: [] as Flight[] }),
18+
withRedux({
19+
actions: {
20+
public: {
21+
loadFlights: payload<{ from: string; to: string }>(),
22+
delayFirst: noPayload,
23+
},
24+
private: {
25+
flightsLoaded: payload<{ flights: Flight[] }>(),
26+
},
27+
},
28+
29+
reducer: (actions, on) => {
30+
on(actions.flightsLoaded, ({ flights }, state) => {
31+
patchState(state, 'flights loaded', { flights });
32+
});
33+
},
34+
35+
effects: (actions, create) => {
36+
const httpClient = inject(HttpClient);
37+
38+
return {
39+
loadFlights$: create(actions.loadFlights).pipe(
40+
switchMap(({ from, to }) => {
41+
return httpClient.get<Flight[]>(
42+
'https://demo.angulararchitects.io/api/flight',
43+
{
44+
params: new HttpParams().set('from', from).set('to', to),
45+
}
46+
);
47+
}),
48+
map((flights) => actions.flightsLoaded({ flights }))
49+
),
50+
};
51+
},
52+
})
53+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface Flight {
2+
id: number;
3+
from: string;
4+
to: string;
5+
delayed: boolean;
6+
date: Date;
7+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
2+
<!-- Checkbox Column -->
3+
<ng-container matColumnDef="finished">
4+
<mat-header-cell *matHeaderCellDef></mat-header-cell>
5+
<mat-cell *matCellDef="let row" class="actions">
6+
<mat-checkbox
7+
(click)="$event.stopPropagation()"
8+
(change)="checkboxLabel(row)"
9+
[checked]="row.finished"
10+
>
11+
</mat-checkbox>
12+
<mat-icon (click)="removeTodo(row)">delete</mat-icon>
13+
</mat-cell>
14+
</ng-container>
15+
16+
<!-- Name Column -->
17+
<ng-container matColumnDef="name">
18+
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
19+
<mat-cell *matCellDef="let element">{{ element.name }}</mat-cell>
20+
</ng-container>
21+
22+
<!-- Description Column -->
23+
<ng-container matColumnDef="description">
24+
<mat-header-cell *matHeaderCellDef>Description</mat-header-cell>
25+
<mat-cell *matCellDef="let element">{{ element.description }}</mat-cell>
26+
</ng-container>
27+
28+
<!-- Deadline Column -->
29+
<ng-container matColumnDef="deadline">
30+
<mat-header-cell mat-header-cell *matHeaderCellDef
31+
>Deadline</mat-header-cell
32+
>
33+
<mat-cell mat-cell *matCellDef="let element">{{
34+
element.deadline
35+
}}</mat-cell>
36+
</ng-container>
37+
38+
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
39+
<mat-row
40+
*matRowDef="let row; columns: displayedColumns"
41+
(click)="selection.toggle(row)"
42+
></mat-row>
43+
</mat-table>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.actions{
2+
display: flex;
3+
align-items: center;
4+
5+
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Component, effect, inject } from '@angular/core';
2+
import { MatCheckboxModule } from '@angular/material/checkbox';
3+
import { MatIconModule } from '@angular/material/icon';
4+
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
5+
import { Todo, TodoStore } from '../todo-store';
6+
import { CategoryStore } from '../category.store';
7+
import { SelectionModel } from '@angular/cdk/collections';
8+
9+
@Component({
10+
selector: 'demo-todo',
11+
templateUrl: 'todo.component.html',
12+
styleUrl: 'todo.component.scss',
13+
standalone: true,
14+
imports: [MatCheckboxModule, MatIconModule, MatTableModule],
15+
})
16+
export class TodoComponent {
17+
todoStore = inject(TodoStore);
18+
categoryStore = inject(CategoryStore);
19+
20+
displayedColumns: string[] = ['finished', 'name', 'description', 'deadline'];
21+
dataSource = new MatTableDataSource<Todo>([]);
22+
selection = new SelectionModel<Todo>(true, []);
23+
24+
constructor() {
25+
effect(() => {
26+
this.dataSource.data = this.todoStore.entities();
27+
});
28+
}
29+
30+
checkboxLabel(todo: Todo) {
31+
this.todoStore.toggleFinished(todo.id);
32+
}
33+
34+
removeTodo(todo: Todo) {
35+
this.todoStore.remove(todo.id);
36+
}
37+
}

libs/ngrx-toolkit/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { withDevtools, patchState, Action } from './lib/with-devtools';
2+
export * from './lib/with-redux';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ActionsFnSpecs } from '../with-redux';
2+
3+
export function assertActionFnSpecs(
4+
obj: unknown
5+
): asserts obj is ActionsFnSpecs {
6+
if (!obj || typeof obj !== 'object') {
7+
throw new Error('%o is not an Action Specification');
8+
}
9+
}

0 commit comments

Comments
 (0)