Skip to content

Commit 4b219b4

Browse files
committed
feat: devtools integration, demo app
1 parent 06a87ce commit 4b219b4

21 files changed

+310
-38
lines changed

projects/shell/src/app/app.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ export const appConfig: ApplicationConfig = {
1717
provideStore(),
1818
provideEffects(),
1919
provideRouterFeature(),
20-
isDevMode() ? provideStoreDevtools() : []
20+
// isDevMode() ? provideStoreDevtools() : []
2121
]
2222
};

projects/shell/src/app/booking/flight/+state/ngrx-signals/tickets.signal.store.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { createReduxState, mapAction, withActionMappers } from '@angular-architects/ngrx-extensions';
2-
import { reduxMethod } from '@angular-architects/ngrx-extensions/rxjs-interop';
1+
import { createReduxState, mapAction, withActionMappers } from '@angular-architects/ngrx-toolkit';
2+
import { reduxMethod } from '@angular-architects/ngrx-toolkit/rxjs-interop';
33
import { computed, inject } from '@angular/core';
44
import { patchState, signalStore, type, withComputed, withMethods } from '@ngrx/signals';
55
import { removeAllEntities, setAllEntities, updateEntity, withEntities } from '@ngrx/signals/entities';

projects/shell/src/app/booking/flight/features/flight-search/flight-search.component.html

+1-5
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ <h2 class="card-title">Flight Search</h2>
1414
{{ localState.flights().length }} flights found!
1515
</div>
1616
}
17-
@if (localState.flights().length) {
18-
<button (click)="delay()"
19-
class="btn btn-default flight-filter-button"
20-
>Delay</button>
21-
}
2217
@if (localState.flights().length) {
2318
<button (click)="reset()"
2419
class="btn btn-default flight-filter-button"
@@ -36,6 +31,7 @@ <h2 class="card-title">Flight Search</h2>
3631
[item]="flight"
3732
[selected]="localState.basket()[flight.id]"
3833
(selectedChange)="updateBasket(flight.id, $event)"
34+
(delayTrigger)="delay($event)"
3935
/>
4036
</div>
4137
}

projects/shell/src/app/booking/flight/features/flight-search/flight-search.component.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ export class FlightSearchComponent {
7171
}));
7272
}
7373

74-
protected delay(): void {
75-
const oldFlight = this.localState.flights()[0];
74+
protected delay(flight: Flight): void {
75+
const oldFlight = flight;
7676
const oldDate = new Date(oldFlight.date);
7777

7878
const newDate = new Date(oldDate.getTime() + 1000 * 60 * 5); // Add 5 min

projects/shell/src/app/booking/flight/flight.routes.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FlightEditComponent } from "./features/flight-edit/flight-edit.componen
55
import { FlightSearchComponent } from "./features/flight-search/flight-search.component";
66
import { FlightTypeaheadComponent } from "./features/flight-typeahead/flight-typeahead.component";
77
import { flightsResolverConfig } from "./logic/data-access/flight.resolver";
8+
import { isDevMode } from "@angular/core";
89

910

1011
export const FLIGHT_ROUTES: Routes = [
@@ -14,7 +15,7 @@ export const FLIGHT_ROUTES: Routes = [
1415
providers: [
1516
// provideState(ticketFeature),
1617
// provideEffects([TicketEffects]),
17-
provideTicketStore()
18+
provideTicketStore(isDevMode())
1819
],
1920
children: [
2021
{

projects/shell/src/app/booking/flight/ui/flight-card/flight-card.component.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { DatePipe, NgStyle } from '@angular/common';
22
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
33
import { RouterLink } from '@angular/router';
4-
import { Flight } from './../../logic/model/flight';
54
import { injectCdBlink } from '../../../../shared/cd-visualizer/cd-visualizer';
5+
import { Flight } from './../../logic/model/flight';
66

77

88
@Component({
@@ -29,16 +29,18 @@ import { injectCdBlink } from '../../../../shared/cd-visualizer/cd-visualizer';
2929
<button
3030
(click)="toggleSelection()"
3131
class="btn btn-info btn-sm"
32-
style="min-width: 85px; margin-right: 5px">
33-
{{ selected ? "Remove" : "Select" }}
34-
</button>
32+
style="min-width: 85px; margin-right: 5px"
33+
>{{ selected ? "Remove" : "Select" }}</button>
3534
<a
3635
[routerLink]="['../edit', item?.id]"
3736
class="btn btn-success btn-sm"
3837
style="min-width: 85px; margin-right: 5px"
39-
>
40-
Edit
41-
</a>
38+
>Edit</a>
39+
<button
40+
(click)="delay()"
41+
class="btn btn-danger btn-sm"
42+
style="min-width: 85px; margin-right: 5px"
43+
>Delay</button>
4244
</p>
4345
</div>
4446
</div>
@@ -52,9 +54,14 @@ export class FlightCardComponent {
5254
@Input() item?: Flight;
5355
@Input() selected = false;
5456
@Output() selectedChange = new EventEmitter<boolean>();
57+
@Output() delayTrigger = new EventEmitter<Flight>();
5558

5659
toggleSelection(): void {
5760
this.selected = !this.selected;
5861
this.selectedChange.emit(this.selected);
5962
}
63+
64+
delay(): void {
65+
this.delayTrigger.emit(this.item);
66+
}
6067
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { createReduxState, mapAction, withActionMappers } from '@angular-architects/ngrx-toolkit';
2+
import { patchState, signalStore, type, withMethods } from '@ngrx/signals';
3+
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
4+
import { createActionGroup, props } from '@ngrx/store';
5+
import { Passenger } from './../logic/model/passenger';
6+
7+
8+
export const PassengerStore = signalStore(
9+
{ providedIn: 'root' },
10+
// State
11+
withEntities({ entity: type<Passenger>(), collection: 'passenger' }),
12+
// Updater
13+
withMethods(store => ({
14+
setPassengers: (state: { passengers: Passenger[] }) => patchState(store,
15+
setAllEntities(state.passengers, { collection: 'passenger' })),
16+
})),
17+
);
18+
19+
export const passengerActions = createActionGroup({
20+
source: 'passenger',
21+
events: {
22+
'passengers loaded': props<{ passengers: Passenger[] }>()
23+
}
24+
});
25+
26+
export const { providePassengerStore, injectPassengerStore } =
27+
createReduxState('passenger', PassengerStore, store => withActionMappers(
28+
mapAction(passengerActions.passengersLoaded, store.setPassengers)
29+
)
30+
);

projects/shell/src/app/booking/passenger/features/passenger-search/passenger-search.component.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Component, inject } from '@angular/core';
33
import { FormsModule } from '@angular/forms';
44
import { RouterLink } from '@angular/router';
55
import { PassengerService } from '../../logic/data-access/passenger.service';
6-
import { Passenger } from '../../logic/model/passenger';
6+
import { Passenger, initialPassenger } from '../../logic/model/passenger';
7+
import { injectPassengerStore, passengerActions } from '../../+state/passenger.signal.store';
78

89

910
@Component({
@@ -17,6 +18,8 @@ import { Passenger } from '../../logic/model/passenger';
1718
templateUrl: './passenger-search.component.html'
1819
})
1920
export class PassengerSearchComponent {
21+
private store = injectPassengerStore();
22+
2023
firstname = '';
2124
lastname = 'Smith';
2225
selectedPassenger?: Passenger;
@@ -26,6 +29,14 @@ export class PassengerSearchComponent {
2629
return this.#passengerService.passengers;
2730
}
2831

32+
constructor() {
33+
this.store.dispatch(
34+
passengerActions.passengersLoaded({
35+
passengers: [initialPassenger]
36+
})
37+
);
38+
}
39+
2940
search(): void {
3041
if (!(this.firstname || this.lastname)) return;
3142

projects/shell/src/app/booking/passenger/passenger.routes.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Routes } from "@angular/router";
22
import { PassengerSearchComponent } from "./features/passenger-search/passenger-search.component";
33
import { PassengerEditComponent } from "./features/passenger-edit/passenger-edit.component";
4+
import { providePassengerStore } from "./+state/passenger.signal.store";
5+
import { isDevMode } from "@angular/core";
46

57

68
export const PASSENGER_ROUTES: Routes = [
79
{
810
path: '',
11+
providers: [
12+
providePassengerStore(isDevMode())
13+
],
914
children: [
1015
{
1116
path: '',

projects/shell/src/app/core/ui/sidebar/sidebar.component.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
2-
import { Component, inject } from '@angular/core';
2+
import { Component } from '@angular/core';
33
import { RouterLinkActive, RouterLinkWithHref } from '@angular/router';
4-
import { FlightService } from '../../../booking/flight/logic/data-access/flight.service';
54
import { injectTicketStore } from '../../../booking/flight/+state/ngrx-signals/tickets.signal.store';
65

76

projects/shell/src/app/shared/ngrx-signals-redux/create-redux.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ENVIRONMENT_INITIALIZER, inject, makeEnvironmentProviders } from "@angular/core";
22
import { ActionCreator, ActionType } from "@ngrx/store/src/models";
3+
import { addStoreToReduxDevtools } from "../redux-devtools";
34
import { CreateReduxState, ExtractActionTypes, MapperTypes, Store } from "./model";
45
import { SignalReduxStore, injectReduxDispatch } from "./signal-redux-store";
56
import { capitalize, isActionCreator } from "./util";
@@ -80,17 +81,22 @@ export function createReduxState<
8081
// TODO: Internal API access. Provider info needs to be accessible from signalStore.
8182
const isRootProvider = (signalStore as any)?.ɵprov?.providedIn === 'root';
8283
return {
83-
[`provide${capitalize(storeName)}Store`]: () => makeEnvironmentProviders([
84+
[`provide${capitalize(storeName)}Store`]: (connectReduxDevtools = false) => makeEnvironmentProviders([
8485
isRootProvider? [] : signalStore,
8586
{
8687
provide: ENVIRONMENT_INITIALIZER,
8788
multi: true,
8889
useFactory: (
8990
signalReduxStore = inject(SignalReduxStore),
9091
store = inject(signalStore)
91-
) => () => signalReduxStore.connectFeatureStore(
92-
withActionMappers(store)
93-
)
92+
) => () => {
93+
if (connectReduxDevtools) {
94+
addStoreToReduxDevtools(store, storeName, false);
95+
}
96+
signalReduxStore.connectFeatureStore(
97+
withActionMappers(store)
98+
);
99+
}
94100
}
95101
]),
96102
[`inject${capitalize(storeName)}Store`]: () => Object.assign(

projects/shell/src/app/shared/ngrx-signals-redux/model.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type CreateReduxState<
3030
StoreName extends string,
3131
STORE extends Store
3232
> = {
33-
[K in StoreName as `provide${Capitalize<K>}Store`]: () => EnvironmentProviders
33+
[K in StoreName as `provide${Capitalize<K>}Store`]: (connectReduxDevtools?: boolean) => EnvironmentProviders
3434
} & {
3535
[K in StoreName as `inject${Capitalize<K>}Store`]: () => InjectableReduxSlice<STORE>
3636
};

projects/shell/src/app/shared/ngrx-signals-redux/rxjs-interop/redux-method.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Injector, Signal, inject } from "@angular/core";
22
import { rxMethod } from "@ngrx/signals/rxjs-interop";
3-
import { Observable, Unsubscribable, map, pipe, tap } from "rxjs";
4-
import { SignalReduxStore } from "../signal-redux-store";
3+
import { Observable, Unsubscribable, map, pipe } from "rxjs";
54

65

76
type RxMethodInput<Input> = Input | Observable<Input> | Signal<Input>;
@@ -33,7 +32,6 @@ export function reduxMethod<Input, MethodInput = Input, MethodResult = unknown>(
3332
}
3433
): RxMethod<Input, MethodInput, MethodResult> {
3534
const injector = inject(Injector);
36-
const store = inject(SignalReduxStore);
3735

3836
if (typeof resultMethodOrConfig === 'function') {
3937
let unsubscribable: Unsubscribable;
@@ -44,8 +42,7 @@ export function reduxMethod<Input, MethodInput = Input, MethodResult = unknown>(
4442

4543
const rxMethodWithResult = rxMethod<Input>(pipe(
4644
generator,
47-
map(resultMethod),
48-
tap(action => store.dispatch(action as any))
45+
map(resultMethod)
4946
), {
5047
...(config || {}),
5148
injector: config?.injector || injector

projects/shell/src/app/shared/ngrx-signals-redux/signal-redux-store.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Injectable, inject } from "@angular/core";
22
import { rxMethod } from "@ngrx/signals/rxjs-interop";
33
import { Action, ActionCreator } from "@ngrx/store";
4-
import { map, pipe, tap } from "rxjs";
4+
import { pipe, tap } from "rxjs";
5+
import { dispatchActionToReduxDevtools } from "../redux-devtools";
56
import { MapperTypes } from "./model";
67
import { isUnsubscribable } from "./util";
78

@@ -15,7 +16,7 @@ import { isUnsubscribable } from "./util";
1516
export class SignalReduxStore {
1617
private mapperDict: Record<string, {
1718
storeMethod: (...args: unknown[]) => unknown,
18-
resultMethod?: (...args: unknown[]) => unknown
19+
resultMethod?: (...args: unknown[]) => unknown,
1920
}> = {};
2021

2122
dispatch = rxMethod<Action>(pipe(
@@ -26,14 +27,19 @@ export class SignalReduxStore {
2627
isUnsubscribable(callbacks.storeMethod) &&
2728
callbacks.resultMethod
2829
) {
29-
return callbacks.storeMethod(action, callbacks.resultMethod) as any;
30+
return callbacks.storeMethod(action, (a: Action) => {
31+
const resultAction = callbacks.resultMethod?.(a) as Action;
32+
this.dispatch(resultAction);
33+
});
3034
}
3135

3236
return callbacks?.storeMethod(action);
3337
}
3438

35-
return action;
36-
})
39+
return;
40+
}),
41+
//TODO: Refactor to DI token with optional callback
42+
tap(action => dispatchActionToReduxDevtools(action))
3743
));
3844

3945
connectFeatureStore(mappers: MapperTypes<ActionCreator<any, any>[]>[]): void {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getConnection, getRootState, initDevtools, setUntrackedStore, updateStoreRegistry } from "./devtools-core";
2+
import { Action } from "./model";
3+
import { getStoreSignal } from "./util";
4+
5+
6+
/**
7+
* Devtools Public API: Add Store, Dispatch Action
8+
*/
9+
export function addStoreToReduxDevtools(store: unknown, name: string, live = true): boolean {
10+
if (!initDevtools()) {
11+
return false;
12+
}
13+
14+
!live && setUntrackedStore(name);
15+
const storeSignal = getStoreSignal(store);
16+
updateStoreRegistry((value) => ({
17+
...value,
18+
[name]: storeSignal
19+
}));
20+
21+
return true;
22+
}
23+
24+
export function dispatchActionToReduxDevtools(action: Action): void {
25+
getConnection()?.send(action, getRootState());
26+
}

0 commit comments

Comments
 (0)