From 48f9d95c8435f9c86111959f3a7d3f7205a92921 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Sun, 18 Aug 2024 12:06:03 +0200 Subject: [PATCH] feat: add secondary entry point for redux connector The redux-connector is the only extension that requires the @ngrx/store. By making it a secondary entry point, the @ngrx/store dependency is not applied to the other extensions. This fixes issue #79 --- README.md | 52 ++- apps/demo/src/app/category.store.ts | 2 +- .../flight-booking.store.ts | 18 +- .../flight-booking-simple.store.ts | 16 +- .../+state/redux.ts | 34 +- .../+state/store.ts | 63 ++- .../flight-store.ts | 6 +- .../src/app/flight-search/flight-store.ts | 2 +- apps/demo/src/app/shared/flight.service.ts | 13 +- .../todo-storage-sync/synced-todo-store.ts | 2 +- apps/demo/src/app/todo-store.ts | 2 +- libs/ngrx-toolkit/README.md | 430 +----------------- libs/ngrx-toolkit/package.json | 9 +- libs/ngrx-toolkit/redux-connector/index.ts | 6 + .../redux-connector/ng-package.json | 5 + .../src/lib}/create-redux.ts | 0 .../src/lib}/model.ts | 0 .../src/lib}/rxjs-interop/redux-method.ts | 0 .../src/lib}/signal-redux-store.ts | 0 .../src/lib}/util.ts | 0 libs/ngrx-toolkit/src/index.ts | 3 - .../src/lib/redux-connector/index.ts | 2 - .../lib/redux-connector/rxjs-interop/index.ts | 2 - tsconfig.base.json | 4 +- 24 files changed, 147 insertions(+), 524 deletions(-) create mode 100644 libs/ngrx-toolkit/redux-connector/index.ts create mode 100644 libs/ngrx-toolkit/redux-connector/ng-package.json rename libs/ngrx-toolkit/{src/lib/redux-connector => redux-connector/src/lib}/create-redux.ts (100%) rename libs/ngrx-toolkit/{src/lib/redux-connector => redux-connector/src/lib}/model.ts (100%) rename libs/ngrx-toolkit/{src/lib/redux-connector => redux-connector/src/lib}/rxjs-interop/redux-method.ts (100%) rename libs/ngrx-toolkit/{src/lib/redux-connector => redux-connector/src/lib}/signal-redux-store.ts (100%) rename libs/ngrx-toolkit/{src/lib/redux-connector => redux-connector/src/lib}/util.ts (100%) delete mode 100644 libs/ngrx-toolkit/src/lib/redux-connector/index.ts delete mode 100644 libs/ngrx-toolkit/src/lib/redux-connector/rxjs-interop/index.ts diff --git a/README.md b/README.md index 43d8945..eb21d0f 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Starting with 18.0.0-rc.2, we have a [strict version dependency](#why-is-the-ver | @ngrx/signals | @angular-architects/ngrx-toolkit | |----------------|----------------------------------| -| 18.0.2 | 18.0.2 | +| 18.0.2 | latest | | 18.0.0 | 18.0.0 | | 18.0.0-rc.3 | (not supported) | | 18.0.0-rc.2 | 18.0.0-rc.2.x | @@ -35,7 +35,6 @@ To install it, run npm i @angular-architects/ngrx-toolkit ``` - - [NgRx Toolkit](#ngrx-toolkit) - [Devtools: `withDevtools()`](#devtools-withdevtools) - [Redux: `withRedux()`](#redux-withredux) @@ -54,7 +53,6 @@ npm i @angular-architects/ngrx-toolkit - [I have an idea for a new extension, can I contribute?](#i-have-an-idea-for-a-new-extension-can-i-contribute) - [I require a feature that is not available in a lower version. What should I do?](#i-require-a-feature-that-is-not-available-in-a-lower-version-what-should-i-do) - ## Devtools: `withDevtools()` This extension is very easy to use. Just add it to a `signalStore`. Example: @@ -68,13 +66,16 @@ export const FlightStore = signalStore( ); ``` -The Signal Store does not use the Redux pattern, so there are no action names involved by default. Instead, every action is referred to as a β€œStore Update.” However, if you want to customize the action name for better clarity, you can use the `updateState` method instead of `patchState`: +The Signal Store does not use the Redux pattern, so there are no action names involved by default. Instead, every action is referred to as a "Store Update". However, if you want to customize the action name for better clarity, you can use the `updateState` method instead of `patchState`: ```typescript -patchState(this.store, {loading: false}); +patchState(this.store, { loading: false }); // updateState is a wrapper around patchState and has an action name as second parameter -updateState(this.store 'update loading', {loading: false}); +updateState(this.store +'update loading', { loading: false } +) +; ``` ## Redux: `withRedux()` @@ -347,7 +348,7 @@ public class UndoRedoComponent { ## Redux Connector for the NgRx Signal Store `createReduxState()` -The Redux Connector turns any `signalStore()` into a Gobal State Management Slice following the Redux pattern. +The Redux Connector turns any `signalStore()` into a Global State Management Slice following the Redux pattern. It is available as secondary entry point, i.e. `import { createReduxState } from '@angular-architects/ngrx-toolkit/redux-connector'` and has a dependency to `@ngrx/store`. It supports: @@ -357,7 +358,6 @@ It supports: βœ… Auto-generated `provideNamedStore()` & `injectNamedStore()` Functions \ βœ… Global Action to Store Method Mappers \ - ### Use a present Signal Store ```typescript @@ -412,23 +412,23 @@ export const ticketActions = createActionGroup({ ```typescript export const { provideFlightStore, injectFlightStore } = createReduxState('flight', FlightStore, store => withActionMappers( - mapAction( - // Filtered Action - ticketActions.flightsLoad, - // Side-Effect - store.loadFlights, - // Result Action - ticketActions.flightsLoaded), - mapAction( - // Filtered Actions - ticketActions.flightsLoaded, ticketActions.flightsLoadedByPassenger, - // State Updater Method (like Reducers) - store.setFlights - ), - mapAction(ticketActions.flightUpdate, store.updateFlight), - mapAction(ticketActions.flightsClear, store.clearFlights), - ) -); + mapAction( + // Filtered Action + ticketActions.flightsLoad, + // Side-Effect + store.loadFlights, + // Result Action + ticketActions.flightsLoaded), + mapAction( + // Filtered Actions + ticketActions.flightsLoaded, ticketActions.flightsLoadedByPassenger, + // State Updater Method (like Reducers) + store.setFlights + ), + mapAction(ticketActions.flightUpdate, store.updateFlight), + mapAction(ticketActions.flightsClear, store.clearFlights), + ) + ); ``` ### Register an Angular Dependency Injection Provider @@ -476,6 +476,7 @@ export class FlightSearchReducConnectorComponent { } } ``` + ## FAQ ### Why is the version range to the `@ngrx/signals` dependency so strict? @@ -489,6 +490,7 @@ To ensure stability, we clone these internal types and run integration tests for Yes, please! We are always looking for new ideas and contributions. Since we don't want to bloat the library, we are very selective about new features. You also have to provide the following: + - Good test coverage so that we can update it properly and don't have to call you πŸ˜‰. - A use case showing the feature in action in the demo app of the repository. - An entry to the README.md. diff --git a/apps/demo/src/app/category.store.ts b/apps/demo/src/app/category.store.ts index f98c5b3..a9e710e 100644 --- a/apps/demo/src/app/category.store.ts +++ b/apps/demo/src/app/category.store.ts @@ -1,6 +1,6 @@ import { patchState, signalStore, withHooks } from '@ngrx/signals'; import { setAllEntities, withEntities } from '@ngrx/signals/entities'; -import { withDevtools } from 'ngrx-toolkit'; +import { withDevtools } from '@angular-architects/ngrx-toolkit'; export interface Category { id: number; diff --git a/apps/demo/src/app/flight-search-data-service-dynamic/flight-booking.store.ts b/apps/demo/src/app/flight-search-data-service-dynamic/flight-booking.store.ts index 2bbf7b5..913ffe4 100644 --- a/apps/demo/src/app/flight-search-data-service-dynamic/flight-booking.store.ts +++ b/apps/demo/src/app/flight-search-data-service-dynamic/flight-booking.store.ts @@ -1,28 +1,30 @@ import { FlightService } from '../shared/flight.service'; -import { - signalStore, type, -} from '@ngrx/signals'; +import { signalStore, type } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; -import { withCallState, withDataService, withUndoRedo } from 'ngrx-toolkit'; +import { + withCallState, + withDataService, + withUndoRedo, +} from '@angular-architects/ngrx-toolkit'; import { Flight } from '../shared/flight'; export const FlightBookingStore = signalStore( { providedIn: 'root' }, withCallState({ - collection: 'flight' + collection: 'flight', }), withEntities({ entity: type(), - collection: 'flight' + collection: 'flight', }), withDataService({ dataServiceType: FlightService, filter: { from: 'Paris', to: 'New York' }, - collection: 'flight' + collection: 'flight', }), withUndoRedo({ collections: ['flight'], - }), + }) ); diff --git a/apps/demo/src/app/flight-search-data-service-simple/flight-booking-simple.store.ts b/apps/demo/src/app/flight-search-data-service-simple/flight-booking-simple.store.ts index b7a98c8..86963f2 100644 --- a/apps/demo/src/app/flight-search-data-service-simple/flight-booking-simple.store.ts +++ b/apps/demo/src/app/flight-search-data-service-simple/flight-booking-simple.store.ts @@ -1,11 +1,13 @@ import { FlightService } from '../shared/flight.service'; -import { - signalStore, -} from '@ngrx/signals'; +import { signalStore } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; -import { withCallState, withDataService, withUndoRedo } from 'ngrx-toolkit'; +import { + withCallState, + withDataService, + withUndoRedo, +} from '@angular-architects/ngrx-toolkit'; import { Flight } from '../shared/flight'; export const SimpleFlightBookingStore = signalStore( @@ -13,8 +15,8 @@ export const SimpleFlightBookingStore = signalStore( withCallState(), withEntities(), withDataService({ - dataServiceType: FlightService, + dataServiceType: FlightService, filter: { from: 'Paris', to: 'New York' }, }), - withUndoRedo(), -); \ No newline at end of file + withUndoRedo() +); diff --git a/apps/demo/src/app/flight-search-redux-connector/+state/redux.ts b/apps/demo/src/app/flight-search-redux-connector/+state/redux.ts index ccb0e72..d6a3e61 100644 --- a/apps/demo/src/app/flight-search-redux-connector/+state/redux.ts +++ b/apps/demo/src/app/flight-search-redux-connector/+state/redux.ts @@ -1,7 +1,10 @@ -import { createReduxState, withActionMappers, mapAction } from "ngrx-toolkit"; -import { ticketActions } from "./actions"; -import { FlightStore } from "./store"; - +import { ticketActions } from './actions'; +import { FlightStore } from './store'; +import { + createReduxState, + withActionMappers, + mapAction, +} from '@angular-architects/ngrx-toolkit/redux-connector'; export const { provideFlightStore, injectFlightStore } = /** @@ -12,10 +15,19 @@ export const { provideFlightStore, injectFlightStore } = * - Selector Signals * - Dispatch */ - createReduxState('flight', FlightStore, store => withActionMappers( - mapAction(ticketActions.flightsLoad, store.loadFlights, ticketActions.flightsLoaded), - mapAction(ticketActions.flightsLoaded, ticketActions.flightsLoadedByPassenger, store.setFlights), - mapAction(ticketActions.flightUpdate, store.updateFlight), - mapAction(ticketActions.flightsClear, store.clearFlights), - ) -); + createReduxState('flight', FlightStore, (store) => + withActionMappers( + mapAction( + ticketActions.flightsLoad, + store.loadFlights, + ticketActions.flightsLoaded + ), + mapAction( + ticketActions.flightsLoaded, + ticketActions.flightsLoadedByPassenger, + store.setFlights + ), + mapAction(ticketActions.flightUpdate, store.updateFlight), + mapAction(ticketActions.flightsClear, store.clearFlights) + ) + ); diff --git a/apps/demo/src/app/flight-search-redux-connector/+state/store.ts b/apps/demo/src/app/flight-search-redux-connector/+state/store.ts index d882323..d1a8121 100644 --- a/apps/demo/src/app/flight-search-redux-connector/+state/store.ts +++ b/apps/demo/src/app/flight-search-redux-connector/+state/store.ts @@ -1,12 +1,22 @@ import { computed, inject } from '@angular/core'; -import { patchState, signalStore, type, withComputed, withMethods } from '@ngrx/signals'; -import { removeAllEntities, setAllEntities, updateEntity, withEntities } from '@ngrx/signals/entities'; -import { reduxMethod } from 'ngrx-toolkit'; +import { + patchState, + signalStore, + type, + withComputed, + withMethods, +} from '@ngrx/signals'; +import { + removeAllEntities, + setAllEntities, + updateEntity, + withEntities, +} from '@ngrx/signals/entities'; +import { reduxMethod } from '@angular-architects/ngrx-toolkit/redux-connector'; import { from, map, pipe, switchMap } from 'rxjs'; import { Flight } from '../../shared/flight'; import { FlightFilter, FlightService } from '../../shared/flight.service'; - export const FlightStore = signalStore( { providedIn: 'root' }, // State @@ -14,26 +24,39 @@ export const FlightStore = signalStore( withEntities({ entity: type(), collection: 'hide' }), // Selectors withComputed(({ flightEntities, hideEntities }) => ({ - filteredFlights: computed(() => flightEntities() - .filter(flight => !hideEntities().includes(flight.id))), + filteredFlights: computed(() => + flightEntities().filter((flight) => !hideEntities().includes(flight.id)) + ), flightCount: computed(() => flightEntities().length), })), // Updater - withMethods(store => ({ - setFlights: (state: { flights: Flight[] }) => patchState(store, - setAllEntities(state.flights, { collection: 'flight' })), - updateFlight: (state: { flight: Flight }) => patchState(store, - updateEntity({ id: state.flight.id, changes: state.flight }, { collection: 'flight' })), - clearFlights: () => patchState(store, - removeAllEntities({ collection: 'flight' })), + withMethods((store) => ({ + setFlights: (state: { flights: Flight[] }) => + patchState( + store, + setAllEntities(state.flights, { collection: 'flight' }) + ), + updateFlight: (state: { flight: Flight }) => + patchState( + store, + updateEntity( + { id: state.flight.id, changes: state.flight }, + { collection: 'flight' } + ) + ), + clearFlights: () => + patchState(store, removeAllEntities({ collection: 'flight' })), })), // Effects withMethods((store, flightService = inject(FlightService)) => ({ - loadFlights: reduxMethod(pipe( - switchMap(filter => from( - flightService.load({ from: filter.from, to: filter.to }) - )), - map(flights => ({ flights })), - ), store.setFlights), - })), + loadFlights: reduxMethod( + pipe( + switchMap((filter) => + from(flightService.load({ from: filter.from, to: filter.to })) + ), + map((flights) => ({ flights })) + ), + store.setFlights + ), + })) ); diff --git a/apps/demo/src/app/flight-search-with-pagination/flight-store.ts b/apps/demo/src/app/flight-search-with-pagination/flight-store.ts index f174caf..51b402b 100644 --- a/apps/demo/src/app/flight-search-with-pagination/flight-store.ts +++ b/apps/demo/src/app/flight-search-with-pagination/flight-store.ts @@ -3,7 +3,11 @@ import { FlightService } from '../shared/flight.service'; import { signalStore, type } from '@ngrx/signals'; import { withEntities } from '@ngrx/signals/entities'; -import { withCallState, withDataService, withPagination } from 'ngrx-toolkit'; +import { + withCallState, + withDataService, + withPagination, +} from '@angular-architects/ngrx-toolkit'; import { Flight } from '../shared/flight'; export const FlightBookingStore = signalStore( diff --git a/apps/demo/src/app/flight-search/flight-store.ts b/apps/demo/src/app/flight-search/flight-store.ts index 8087b9f..8dc4c53 100644 --- a/apps/demo/src/app/flight-search/flight-store.ts +++ b/apps/demo/src/app/flight-search/flight-store.ts @@ -5,7 +5,7 @@ import { withDevtools, withRedux, updateState, -} from 'ngrx-toolkit'; +} from '@angular-architects/ngrx-toolkit'; import { inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { map, switchMap } from 'rxjs'; diff --git a/apps/demo/src/app/shared/flight.service.ts b/apps/demo/src/app/shared/flight.service.ts index 165222e..44986a2 100644 --- a/apps/demo/src/app/shared/flight.service.ts +++ b/apps/demo/src/app/shared/flight.service.ts @@ -2,16 +2,16 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, firstValueFrom } from 'rxjs'; import { EntityId } from '@ngrx/signals/entities'; -import { DataService } from 'ngrx-toolkit'; +import { DataService } from '@angular-architects/ngrx-toolkit'; import { Flight } from './flight'; export type FlightFilter = { from: string; to: string; -} +}; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FlightService implements DataService { baseUrl = `https://demo.angulararchitects.io/api`; @@ -44,11 +44,7 @@ export class FlightService implements DataService { return firstValueFrom(this.find(filter.from, filter.to)); } - private find( - from: string, - to: string, - urgent = false - ): Observable { + private find(from: string, to: string, urgent = false): Observable { let url = [this.baseUrl, 'flight'].join('/'); if (urgent) { @@ -75,5 +71,4 @@ export class FlightService implements DataService { const url = [this.baseUrl, 'flight', flight.id].join('/'); return this.http.delete(url); } - } diff --git a/apps/demo/src/app/todo-storage-sync/synced-todo-store.ts b/apps/demo/src/app/todo-storage-sync/synced-todo-store.ts index bf4f1f4..206a32a 100644 --- a/apps/demo/src/app/todo-storage-sync/synced-todo-store.ts +++ b/apps/demo/src/app/todo-storage-sync/synced-todo-store.ts @@ -6,7 +6,7 @@ import { updateEntity, } from '@ngrx/signals/entities'; import { AddTodo, Todo } from '../todo-store'; -import { withStorageSync } from 'ngrx-toolkit'; +import { withStorageSync } from '@angular-architects/ngrx-toolkit'; export const SyncedTodoStore = signalStore( { providedIn: 'root' }, diff --git a/apps/demo/src/app/todo-store.ts b/apps/demo/src/app/todo-store.ts index f5ed590..ccd68b5 100644 --- a/apps/demo/src/app/todo-store.ts +++ b/apps/demo/src/app/todo-store.ts @@ -5,7 +5,7 @@ import { updateEntity, withEntities, } from '@ngrx/signals/entities'; -import { updateState, withDevtools } from 'ngrx-toolkit'; +import { updateState, withDevtools } from '@angular-architects/ngrx-toolkit'; export interface Todo { id: number; diff --git a/libs/ngrx-toolkit/README.md b/libs/ngrx-toolkit/README.md index c18bc26..ca906f5 100644 --- a/libs/ngrx-toolkit/README.md +++ b/libs/ngrx-toolkit/README.md @@ -19,432 +19,4 @@ To install it, run npm i @angular-architects/ngrx-toolkit ``` -Starting with 18.0.0-rc.2, we have a [strict version dependency](#why-is-the-version-range-to-the-ngrxsignals-dependency-so-strict) to `@ngrx/signals`: - -| @ngrx/signals | @angular-architects/ngrx-toolkit | -|----------------|----------------------------------| -| <= 18.0.0-rc.1 | 0.0.4 | -| 18.0.0-rc.2 | 18.0.0-rc.2.x | - -To install it, run - -```shell -npm i @angular-architects/ngrx-toolkit -``` - - -- [NgRx Toolkit](#ngrx-toolkit) - - [Devtools: `withDevtools()`](#devtools-withdevtools) - - [Redux: `withRedux()`](#redux-withredux) - - [DataService `withDataService()`](#dataservice-withdataservice) - - [DataService with Dynamic Properties](#dataservice-with-dynamic-properties) - - [Storage Sync `withStorageSync`](#storage-sync-withstoragesync) - - [Redux Connector for the NgRx Signal Store `createReduxState()`](#redux-connector-for-the-ngrx-signal-store-createreduxstate) - - [Use a present Signal Store](#use-a-present-signal-store) - - [Use well-known NgRx Store Actions](#use-well-known-ngrx-store-actions) - - [Map Actions to Methods](#map-actions-to-methods) - - [Register an Angular Dependency Injection Provider](#register-an-angular-dependency-injection-provider) - - [Use the Store in your Component](#use-the-store-in-your-component) - - [FAQ](#faq) - - [Why is the version range to the `@ngrx/signals` dependency so strict?](#why-is-the-version-range-to-the-ngrxsignals-dependency-so-strict) - - [I have an idea for a new extension, can I contribute?](#i-have-an-idea-for-a-new-extension-can-i-contribute) - - [I require a feature that is not available in a lower version. What should I do?](#i-require-a-feature-that-is-not-available-in-a-lower-version-what-should-i-do) - - -## Devtools: `withDevtools()` - -This extension is very easy to use. Just add it to a `signalStore`. Example: - -```typescript -export const FlightStore = signalStore( - { providedIn: 'root' }, - withDevtools('flights'), // <-- add this - withState({ flights: [] as Flight[] }) - // ... -); -``` - -## Redux: `withRedux()` - -`withRedux()` bring back the Redux pattern into the Signal Store. - -It can be combined with any other extension of the Signal Store. - -Example: - -```typescript -export const FlightStore = signalStore( - { providedIn: 'root' }, - withState({ flights: [] as Flight[] }), - withRedux({ - actions: { - public: { - load: payload<{ from: string; to: string }>(), - }, - private: { - loaded: payload<{ flights: Flight[] }>(), - }, - }, - reducer(actions, on) { - on(actions.loaded, ({ flights }, state) => { - patchState(state, 'flights loaded', { flights }); - }); - }, - effects(actions, create) { - const httpClient = inject(HttpClient); - return { - load$: create(actions.load).pipe( - switchMap(({ from, to }) => - httpClient.get('https://demo.angulararchitects.io/api/flight', { - params: new HttpParams().set('from', from).set('to', to), - }) - ), - tap((flights) => actions.loaded({ flights })) - ), - }; - }, - }) -); -``` - -## DataService `withDataService()` - -`withDataService()` allows to connect a Data Service to the store: - -This gives you a store for a CRUD use case: - -```typescript -export const SimpleFlightBookingStore = signalStore( - { providedIn: 'root' }, - withCallState(), - withEntities(), - withDataService({ - dataServiceType: FlightService, - filter: { from: 'Paris', to: 'New York' }, - }), - withUndoRedo() -); -``` - -The features `withCallState` and `withUndoRedo` are optional, but when present, they enrich each other. - -The Data Service needs to implement the `DataService` interface: - -```typescript -@Injectable({ - providedIn: 'root' -}) -export class FlightService implements DataService { - loadById(id: EntityId): Promise { ... } - load(filter: FlightFilter): Promise { ... } - - create(entity: Flight): Promise { ... } - update(entity: Flight): Promise { ... } - updateAll(entity: Flight[]): Promise { ... } - delete(entity: Flight): Promise { ... } - [...] -} -``` - -Once the store is defined, it gives its consumers numerous signals and methods they just need to delegate to: - -```typescript -@Component(...) -export class FlightSearchSimpleComponent { - private store = inject(SimpleFlightBookingStore); - - from = this.store.filter.from; - to = this.store.filter.to; - flights = this.store.entities; - selected = this.store.selectedEntities; - selectedIds = this.store.selectedIds; - - loading = this.store.loading; - - canUndo = this.store.canUndo; - canRedo = this.store.canRedo; - - async search() { - this.store.load(); - } - - undo(): void { - this.store.undo(); - } - - redo(): void { - this.store.redo(); - } - - updateCriteria(from: string, to: string): void { - this.store.updateFilter({ from, to }); - } - - updateBasket(id: number, selected: boolean): void { - this.store.updateSelected(id, selected); - } - -} -``` - -## DataService with Dynamic Properties - -To avoid naming conflicts, the properties set up by `withDataService` and the connected features can be configured in a typesafe way: - -```typescript -export const FlightBookingStore = signalStore( - { providedIn: 'root' }, - withCallState({ - collection: 'flight', - }), - withEntities({ - entity: type(), - collection: 'flight', - }), - withDataService({ - dataServiceType: FlightService, - filter: { from: 'Graz', to: 'Hamburg' }, - collection: 'flight', - }), - withUndoRedo({ - collections: ['flight'], - }) -); -``` - -This setup makes them use `flight` as part of the used property names. As these implementations respect the Type Script type system, the compiler will make sure these properties are used in a typesafe way: - -```typescript -@Component(...) -export class FlightSearchDynamicComponent { - private store = inject(FlightBookingStore); - - from = this.store.flightFilter.from; - to = this.store.flightFilter.to; - flights = this.store.flightEntities; - selected = this.store.selectedFlightEntities; - selectedIds = this.store.selectedFlightIds; - - loading = this.store.flightLoading; - - canUndo = this.store.canUndo; - canRedo = this.store.canRedo; - - async search() { - this.store.loadFlightEntities(); - } - - undo(): void { - this.store.undo(); - } - - redo(): void { - this.store.redo(); - } - - updateCriteria(from: string, to: string): void { - this.store.updateFlightFilter({ from, to }); - } - - updateBasket(id: number, selected: boolean): void { - this.store.updateSelectedFlightEntities(id, selected); - } - -} -``` - -## Storage Sync `withStorageSync()` - -`withStorageSync` adds automatic or manual synchronization with Web Storage (`localstorage`/`sessionstorage`). - -> [!WARNING] -> As Web Storage only works in browser environments it will fallback to a stub implementation on server environments. - -Example: - -```ts -const SyncStore = signalStore( - withStorageSync({ - key: 'synced', // key used when writing to/reading from storage - autoSync: false, // read from storage on init and write on state changes - `true` by default - select: (state: User) => Partial, // projection to keep specific slices in sync - parse: (stateString: string) => State, // custom parsing from storage - `JSON.parse` by default - stringify: (state: User) => string, // custom stringification - `JSON.stringify` by default - storage: () => sessionstorage, // factory to select storage to sync with - }) -); -``` - -```ts -@Component(...) -public class SyncedStoreComponent { - private syncStore = inject(SyncStore); - - updateFromStorage(): void { - this.syncStore.readFromStorage(); // reads the stored item from storage and patches the state - } - - updateStorage(): void { - this.syncStore.writeToStorage(); // writes the current state to storage - } - - clearStorage(): void { - this.syncStore.clearStorage(); // clears the stored item in storage - } -} -``` - -## Redux Connector for the NgRx Signal Store `createReduxState()` - -The Redux Connector turns any `signalStore()` into a Gobal State Management Slice following the Redux pattern. - -It supports: - -βœ… Well-known NgRx Store Actions \ -βœ… Global Action `dispatch()` \ -βœ… Angular Lazy Loading \ -βœ… Auto-generated `provideNamedStore()` & `injectNamedStore()` Functions \ -βœ… Global Action to Store Method Mappers \ - - -### Use a present Signal Store - -```typescript -export const FlightStore = signalStore( - // State - withEntities({ entity: type(), collection: 'flight' }), - withEntities({ entity: type(), collection: 'hide' }), - // Selectors - withComputed(({ flightEntities, hideEntities }) => ({ - filteredFlights: computed(() => flightEntities() - .filter(flight => !hideEntities().includes(flight.id))), - flightCount: computed(() => flightEntities().length), - })), - // Updater - withMethods(store => ({ - setFlights: (state: { flights: Flight[] }) => patchState(store, - setAllEntities(state.flights, { collection: 'flight' })), - updateFlight: (state: { flight: Flight }) => patchState(store, - updateEntity({ id: state.flight.id, changes: state.flight }, { collection: 'flight' })), - clearFlights: () => patchState(store, - removeAllEntities({ collection: 'flight' })), - })), - // Effects - withMethods((store, flightService = inject(FlightService)) => ({ - loadFlights: reduxMethod(pipe( - switchMap(filter => from( - flightService.load({ from: filter.from, to: filter.to }) - )), - map(flights => ({ flights })), - ), store.setFlights), - })), -); -``` - -### Use well-known NgRx Store Actions - -```typescript -export const ticketActions = createActionGroup({ - source: 'tickets', - events: { - 'flights load': props(), - 'flights loaded': props<{ flights: Flight[] }>(), - 'flights loaded by passenger': props<{ flights: Flight[] }>(), - 'flight update': props<{ flight: Flight }>(), - 'flights clear': emptyProps() - } -}); -``` - -### Map Actions to Methods - -```typescript -export const { provideFlightStore, injectFlightStore } = - createReduxState('flight', FlightStore, store => withActionMappers( - mapAction( - // Filtered Action - ticketActions.flightsLoad, - // Side-Effect - store.loadFlights, - // Result Action - ticketActions.flightsLoaded), - mapAction( - // Filtered Actions - ticketActions.flightsLoaded, ticketActions.flightsLoadedByPassenger, - // State Updater Method (like Reducers) - store.setFlights - ), - mapAction(ticketActions.flightUpdate, store.updateFlight), - mapAction(ticketActions.flightsClear, store.clearFlights), - ) -); -``` - -### Register an Angular Dependency Injection Provider - -```typescript -export const appRoutes: Route[] = [ - { - path: 'flight-search-redux-connector', - providers: [provideFlightStore()], - component: FlightSearchReducConnectorComponent - }, -]; -``` - -### Use the Store in your Component - -```typescript -@Component({ - standalone: true, - imports: [ - JsonPipe, - RouterLink, - FormsModule, - FlightCardComponent - ], - selector: 'demo-flight-search-redux-connector', - templateUrl: './flight-search.component.html', -}) -export class FlightSearchReducConnectorComponent { - private store = injectFlightStore(); - - protected flights = this.store.flightEntities; - - protected search() { - this.store.dispatch( - ticketActions.flightsLoad({ - from: this.localState.filter.from(), - to: this.localState.filter.to() - }) - ); - } - - protected reset(): void { - this.store.dispatch(ticketActions.flightsClear()); - } -} -``` -## FAQ - -### Why is the version range to the `@ngrx/signals` dependency so strict? - -The strict version range for @ngrx/signals is necessary because some of our features rely on encapsulated types, which can change even in a patch release. - -To ensure stability, we clone these internal types and run integration tests for each release. This rigorous testing means we may need to update our version, even for a patch release, to maintain compatibility and stability. - -### I have an idea for a new extension, can I contribute? - -Yes, please! We are always looking for new ideas and contributions. - -Since we don't want to bloat the library, we are very selective about new features. You also have to provide the following: -- Good test coverage so that we can update it properly and don't have to call you πŸ˜‰. -- A use case showing the feature in action in the demo app of the repository. -- An entry to the README.md. - -This project uses [pnpm](https://pnpm.io/) to manage dependencies and run tasks (for local development and CI). - -### I require a feature that is not available in a lower version. What should I do? - -Please create an issue. Very likely, we are able to cherry-pick the feature into the lower version. - +For more information, see the [NgRx Toolkit Documentation](https://github.com/angular-architects/ngrx-toolkit/blob/main/README.md/). diff --git a/libs/ngrx-toolkit/package.json b/libs/ngrx-toolkit/package.json index 6e0b132..dd3c7ec 100644 --- a/libs/ngrx-toolkit/package.json +++ b/libs/ngrx-toolkit/package.json @@ -7,8 +7,15 @@ "url": "https://github.com/angular-architects/ngrx-toolkit" }, "peerDependencies": { - "@ngrx/signals": "18.0.2" + "@ngrx/signals": "18.0.2", + "@ngrx/store": ">=18.0.0" }, + "peerDependenciesMeta": { + "@ngrx/store": { + "optional": true + } + }, + "dependencies": { "tslib": "^2.3.0" }, diff --git a/libs/ngrx-toolkit/redux-connector/index.ts b/libs/ngrx-toolkit/redux-connector/index.ts new file mode 100644 index 0000000..d82bb31 --- /dev/null +++ b/libs/ngrx-toolkit/redux-connector/index.ts @@ -0,0 +1,6 @@ +export { + createReduxState, + mapAction, + withActionMappers, +} from './src/lib/create-redux'; +export { reduxMethod } from './src/lib/rxjs-interop/redux-method'; diff --git a/libs/ngrx-toolkit/redux-connector/ng-package.json b/libs/ngrx-toolkit/redux-connector/ng-package.json new file mode 100644 index 0000000..1dc0b0b --- /dev/null +++ b/libs/ngrx-toolkit/redux-connector/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/create-redux.ts b/libs/ngrx-toolkit/redux-connector/src/lib/create-redux.ts similarity index 100% rename from libs/ngrx-toolkit/src/lib/redux-connector/create-redux.ts rename to libs/ngrx-toolkit/redux-connector/src/lib/create-redux.ts diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/model.ts b/libs/ngrx-toolkit/redux-connector/src/lib/model.ts similarity index 100% rename from libs/ngrx-toolkit/src/lib/redux-connector/model.ts rename to libs/ngrx-toolkit/redux-connector/src/lib/model.ts diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/rxjs-interop/redux-method.ts b/libs/ngrx-toolkit/redux-connector/src/lib/rxjs-interop/redux-method.ts similarity index 100% rename from libs/ngrx-toolkit/src/lib/redux-connector/rxjs-interop/redux-method.ts rename to libs/ngrx-toolkit/redux-connector/src/lib/rxjs-interop/redux-method.ts diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/signal-redux-store.ts b/libs/ngrx-toolkit/redux-connector/src/lib/signal-redux-store.ts similarity index 100% rename from libs/ngrx-toolkit/src/lib/redux-connector/signal-redux-store.ts rename to libs/ngrx-toolkit/redux-connector/src/lib/signal-redux-store.ts diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/util.ts b/libs/ngrx-toolkit/redux-connector/src/lib/util.ts similarity index 100% rename from libs/ngrx-toolkit/src/lib/redux-connector/util.ts rename to libs/ngrx-toolkit/redux-connector/src/lib/util.ts diff --git a/libs/ngrx-toolkit/src/index.ts b/libs/ngrx-toolkit/src/index.ts index fe69e84..2eddd51 100644 --- a/libs/ngrx-toolkit/src/index.ts +++ b/libs/ngrx-toolkit/src/index.ts @@ -11,6 +11,3 @@ export * from './lib/with-undo-redo'; export * from './lib/with-data-service'; export { withStorageSync, SyncConfig } from './lib/with-storage-sync'; export * from './lib/with-pagination'; - -export * from './lib/redux-connector'; -export * from './lib/redux-connector/rxjs-interop'; diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/index.ts b/libs/ngrx-toolkit/src/lib/redux-connector/index.ts deleted file mode 100644 index 3f8438e..0000000 --- a/libs/ngrx-toolkit/src/lib/redux-connector/index.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export { createReduxState, mapAction, withActionMappers } from './create-redux'; diff --git a/libs/ngrx-toolkit/src/lib/redux-connector/rxjs-interop/index.ts b/libs/ngrx-toolkit/src/lib/redux-connector/rxjs-interop/index.ts deleted file mode 100644 index e15b6a1..0000000 --- a/libs/ngrx-toolkit/src/lib/redux-connector/rxjs-interop/index.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export { reduxMethod } from './redux-method'; diff --git a/tsconfig.base.json b/tsconfig.base.json index a7ebc84..34de166 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,8 +15,8 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@ngrx-toolkit/nx-toolkit": ["libs/nx-toolkit/src/index.ts"], - "ngrx-toolkit": ["libs/ngrx-toolkit/src/index.ts"] + "@angular-architects/ngrx-toolkit": ["libs/ngrx-toolkit/src/index.ts"], + "@angular-architects/ngrx-toolkit/redux-connector": ["libs/ngrx-toolkit/redux-connector"] } }, "exclude": ["node_modules", "tmp"]