Skip to content

Commit

Permalink
feat: devtools integration, demo app
Browse files Browse the repository at this point in the history
  • Loading branch information
mikezks committed Dec 18, 2023
1 parent 06a87ce commit 4b219b4
Show file tree
Hide file tree
Showing 21 changed files with 310 additions and 38 deletions.
2 changes: 1 addition & 1 deletion projects/shell/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export const appConfig: ApplicationConfig = {
provideStore(),
provideEffects(),
provideRouterFeature(),
isDevMode() ? provideStoreDevtools() : []
// isDevMode() ? provideStoreDevtools() : []
]
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createReduxState, mapAction, withActionMappers } from '@angular-architects/ngrx-extensions';
import { reduxMethod } from '@angular-architects/ngrx-extensions/rxjs-interop';
import { createReduxState, mapAction, withActionMappers } from '@angular-architects/ngrx-toolkit';
import { reduxMethod } from '@angular-architects/ngrx-toolkit/rxjs-interop';
import { computed, inject } from '@angular/core';
import { patchState, signalStore, type, withComputed, withMethods } from '@ngrx/signals';
import { removeAllEntities, setAllEntities, updateEntity, withEntities } from '@ngrx/signals/entities';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ <h2 class="card-title">Flight Search</h2>
{{ localState.flights().length }} flights found!
</div>
}
@if (localState.flights().length) {
<button (click)="delay()"
class="btn btn-default flight-filter-button"
>Delay</button>
}
@if (localState.flights().length) {
<button (click)="reset()"
class="btn btn-default flight-filter-button"
Expand All @@ -36,6 +31,7 @@ <h2 class="card-title">Flight Search</h2>
[item]="flight"
[selected]="localState.basket()[flight.id]"
(selectedChange)="updateBasket(flight.id, $event)"
(delayTrigger)="delay($event)"
/>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export class FlightSearchComponent {
}));
}

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

const newDate = new Date(oldDate.getTime() + 1000 * 60 * 5); // Add 5 min
Expand Down
3 changes: 2 additions & 1 deletion projects/shell/src/app/booking/flight/flight.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FlightEditComponent } from "./features/flight-edit/flight-edit.componen
import { FlightSearchComponent } from "./features/flight-search/flight-search.component";
import { FlightTypeaheadComponent } from "./features/flight-typeahead/flight-typeahead.component";
import { flightsResolverConfig } from "./logic/data-access/flight.resolver";
import { isDevMode } from "@angular/core";


export const FLIGHT_ROUTES: Routes = [
Expand All @@ -14,7 +15,7 @@ export const FLIGHT_ROUTES: Routes = [
providers: [
// provideState(ticketFeature),
// provideEffects([TicketEffects]),
provideTicketStore()
provideTicketStore(isDevMode())
],
children: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { DatePipe, NgStyle } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Flight } from './../../logic/model/flight';
import { injectCdBlink } from '../../../../shared/cd-visualizer/cd-visualizer';
import { Flight } from './../../logic/model/flight';


@Component({
Expand All @@ -29,16 +29,18 @@ import { injectCdBlink } from '../../../../shared/cd-visualizer/cd-visualizer';
<button
(click)="toggleSelection()"
class="btn btn-info btn-sm"
style="min-width: 85px; margin-right: 5px">
{{ selected ? "Remove" : "Select" }}
</button>
style="min-width: 85px; margin-right: 5px"
>{{ selected ? "Remove" : "Select" }}</button>
<a
[routerLink]="['../edit', item?.id]"
class="btn btn-success btn-sm"
style="min-width: 85px; margin-right: 5px"
>
Edit
</a>
>Edit</a>
<button
(click)="delay()"
class="btn btn-danger btn-sm"
style="min-width: 85px; margin-right: 5px"
>Delay</button>
</p>
</div>
</div>
Expand All @@ -52,9 +54,14 @@ export class FlightCardComponent {
@Input() item?: Flight;
@Input() selected = false;
@Output() selectedChange = new EventEmitter<boolean>();
@Output() delayTrigger = new EventEmitter<Flight>();

toggleSelection(): void {
this.selected = !this.selected;
this.selectedChange.emit(this.selected);
}

delay(): void {
this.delayTrigger.emit(this.item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createReduxState, mapAction, withActionMappers } from '@angular-architects/ngrx-toolkit';
import { patchState, signalStore, type, withMethods } from '@ngrx/signals';
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
import { createActionGroup, props } from '@ngrx/store';
import { Passenger } from './../logic/model/passenger';


export const PassengerStore = signalStore(
{ providedIn: 'root' },
// State
withEntities({ entity: type<Passenger>(), collection: 'passenger' }),
// Updater
withMethods(store => ({
setPassengers: (state: { passengers: Passenger[] }) => patchState(store,
setAllEntities(state.passengers, { collection: 'passenger' })),
})),
);

export const passengerActions = createActionGroup({
source: 'passenger',
events: {
'passengers loaded': props<{ passengers: Passenger[] }>()
}
});

export const { providePassengerStore, injectPassengerStore } =
createReduxState('passenger', PassengerStore, store => withActionMappers(
mapAction(passengerActions.passengersLoaded, store.setPassengers)
)
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { PassengerService } from '../../logic/data-access/passenger.service';
import { Passenger } from '../../logic/model/passenger';
import { Passenger, initialPassenger } from '../../logic/model/passenger';
import { injectPassengerStore, passengerActions } from '../../+state/passenger.signal.store';


@Component({
Expand All @@ -17,6 +18,8 @@ import { Passenger } from '../../logic/model/passenger';
templateUrl: './passenger-search.component.html'
})
export class PassengerSearchComponent {
private store = injectPassengerStore();

firstname = '';
lastname = 'Smith';
selectedPassenger?: Passenger;
Expand All @@ -26,6 +29,14 @@ export class PassengerSearchComponent {
return this.#passengerService.passengers;
}

constructor() {
this.store.dispatch(
passengerActions.passengersLoaded({
passengers: [initialPassenger]
})
);
}

search(): void {
if (!(this.firstname || this.lastname)) return;

Expand Down
5 changes: 5 additions & 0 deletions projects/shell/src/app/booking/passenger/passenger.routes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Routes } from "@angular/router";
import { PassengerSearchComponent } from "./features/passenger-search/passenger-search.component";
import { PassengerEditComponent } from "./features/passenger-edit/passenger-edit.component";
import { providePassengerStore } from "./+state/passenger.signal.store";
import { isDevMode } from "@angular/core";


export const PASSENGER_ROUTES: Routes = [
{
path: '',
providers: [
providePassengerStore(isDevMode())
],
children: [
{
path: '',
Expand Down
3 changes: 1 addition & 2 deletions projects/shell/src/app/core/ui/sidebar/sidebar.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component } from '@angular/core';
import { RouterLinkActive, RouterLinkWithHref } from '@angular/router';
import { FlightService } from '../../../booking/flight/logic/data-access/flight.service';
import { injectTicketStore } from '../../../booking/flight/+state/ngrx-signals/tickets.signal.store';


Expand Down
14 changes: 10 additions & 4 deletions projects/shell/src/app/shared/ngrx-signals-redux/create-redux.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ENVIRONMENT_INITIALIZER, inject, makeEnvironmentProviders } from "@angular/core";
import { ActionCreator, ActionType } from "@ngrx/store/src/models";
import { addStoreToReduxDevtools } from "../redux-devtools";
import { CreateReduxState, ExtractActionTypes, MapperTypes, Store } from "./model";
import { SignalReduxStore, injectReduxDispatch } from "./signal-redux-store";
import { capitalize, isActionCreator } from "./util";
Expand Down Expand Up @@ -80,17 +81,22 @@ export function createReduxState<
// TODO: Internal API access. Provider info needs to be accessible from signalStore.
const isRootProvider = (signalStore as any)?.ɵprov?.providedIn === 'root';
return {
[`provide${capitalize(storeName)}Store`]: () => makeEnvironmentProviders([
[`provide${capitalize(storeName)}Store`]: (connectReduxDevtools = false) => makeEnvironmentProviders([
isRootProvider? [] : signalStore,
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory: (
signalReduxStore = inject(SignalReduxStore),
store = inject(signalStore)
) => () => signalReduxStore.connectFeatureStore(
withActionMappers(store)
)
) => () => {
if (connectReduxDevtools) {
addStoreToReduxDevtools(store, storeName, false);
}
signalReduxStore.connectFeatureStore(
withActionMappers(store)
);
}
}
]),
[`inject${capitalize(storeName)}Store`]: () => Object.assign(
Expand Down
2 changes: 1 addition & 1 deletion projects/shell/src/app/shared/ngrx-signals-redux/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type CreateReduxState<
StoreName extends string,
STORE extends Store
> = {
[K in StoreName as `provide${Capitalize<K>}Store`]: () => EnvironmentProviders
[K in StoreName as `provide${Capitalize<K>}Store`]: (connectReduxDevtools?: boolean) => EnvironmentProviders
} & {
[K in StoreName as `inject${Capitalize<K>}Store`]: () => InjectableReduxSlice<STORE>
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Injector, Signal, inject } from "@angular/core";
import { rxMethod } from "@ngrx/signals/rxjs-interop";
import { Observable, Unsubscribable, map, pipe, tap } from "rxjs";
import { SignalReduxStore } from "../signal-redux-store";
import { Observable, Unsubscribable, map, pipe } from "rxjs";


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

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

const rxMethodWithResult = rxMethod<Input>(pipe(
generator,
map(resultMethod),
tap(action => store.dispatch(action as any))
map(resultMethod)
), {
...(config || {}),
injector: config?.injector || injector
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Injectable, inject } from "@angular/core";
import { rxMethod } from "@ngrx/signals/rxjs-interop";
import { Action, ActionCreator } from "@ngrx/store";
import { map, pipe, tap } from "rxjs";
import { pipe, tap } from "rxjs";
import { dispatchActionToReduxDevtools } from "../redux-devtools";
import { MapperTypes } from "./model";
import { isUnsubscribable } from "./util";

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

dispatch = rxMethod<Action>(pipe(
Expand All @@ -26,14 +27,19 @@ export class SignalReduxStore {
isUnsubscribable(callbacks.storeMethod) &&
callbacks.resultMethod
) {
return callbacks.storeMethod(action, callbacks.resultMethod) as any;
return callbacks.storeMethod(action, (a: Action) => {
const resultAction = callbacks.resultMethod?.(a) as Action;
this.dispatch(resultAction);
});
}

return callbacks?.storeMethod(action);
}

return action;
})
return;
}),
//TODO: Refactor to DI token with optional callback
tap(action => dispatchActionToReduxDevtools(action))
));

connectFeatureStore(mappers: MapperTypes<ActionCreator<any, any>[]>[]): void {
Expand Down
26 changes: 26 additions & 0 deletions projects/shell/src/app/shared/redux-devtools/devtools-connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getConnection, getRootState, initDevtools, setUntrackedStore, updateStoreRegistry } from "./devtools-core";
import { Action } from "./model";
import { getStoreSignal } from "./util";


/**
* Devtools Public API: Add Store, Dispatch Action
*/
export function addStoreToReduxDevtools(store: unknown, name: string, live = true): boolean {
if (!initDevtools()) {
return false;
}

!live && setUntrackedStore(name);
const storeSignal = getStoreSignal(store);
updateStoreRegistry((value) => ({
...value,
[name]: storeSignal
}));

return true;
}

export function dispatchActionToReduxDevtools(action: Action): void {
getConnection()?.send(action, getRootState());
}
Loading

0 comments on commit 4b219b4

Please sign in to comment.