Skip to content

Commit

Permalink
feat: add secondary entry point for redux connector
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rainerhahnekamp authored Aug 18, 2024
1 parent 42d098b commit 48f9d95
Show file tree
Hide file tree
Showing 24 changed files with 147 additions and 524 deletions.
52 changes: 27 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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()`
Expand Down Expand Up @@ -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:

Expand All @@ -357,7 +358,6 @@ It supports:
✅ Auto-generated `provideNamedStore()` & `injectNamedStore()` Functions \
✅ Global Action to Store Method Mappers \


### Use a present Signal Store

```typescript
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -476,6 +476,7 @@ export class FlightSearchReducConnectorComponent {
}
}
```

## FAQ

### Why is the version range to the `@ngrx/signals` dependency so strict?
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/app/category.store.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Flight>(),
collection: 'flight'
collection: 'flight',
}),
withDataService({
dataServiceType: FlightService,
filter: { from: 'Paris', to: 'New York' },
collection: 'flight'
collection: 'flight',
}),
withUndoRedo({
collections: ['flight'],
}),
})
);
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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(
{ providedIn: 'root' },
withCallState(),
withEntities<Flight>(),
withDataService({
dataServiceType: FlightService,
dataServiceType: FlightService,
filter: { from: 'Paris', to: 'New York' },
}),
withUndoRedo(),
);
withUndoRedo()
);
34 changes: 23 additions & 11 deletions apps/demo/src/app/flight-search-redux-connector/+state/redux.ts
Original file line number Diff line number Diff line change
@@ -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 } =
/**
Expand All @@ -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)
)
);
63 changes: 43 additions & 20 deletions apps/demo/src/app/flight-search-redux-connector/+state/store.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,62 @@
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
withEntities({ entity: type<Flight>(), collection: 'flight' }),
withEntities({ entity: type<number>(), 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<FlightFilter, { flights: Flight[] }>(pipe(
switchMap(filter => from(
flightService.load({ from: filter.from, to: filter.to })
)),
map(flights => ({ flights })),
), store.setFlights),
})),
loadFlights: reduxMethod<FlightFilter, { flights: Flight[] }>(
pipe(
switchMap((filter) =>
from(flightService.load({ from: filter.from, to: filter.to }))
),
map((flights) => ({ flights }))
),
store.setFlights
),
}))
);
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/app/flight-search/flight-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
13 changes: 4 additions & 9 deletions apps/demo/src/app/shared/flight.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Flight, FlightFilter> {
baseUrl = `https://demo.angulararchitects.io/api`;
Expand Down Expand Up @@ -44,11 +44,7 @@ export class FlightService implements DataService<Flight, FlightFilter> {
return firstValueFrom(this.find(filter.from, filter.to));
}

private find(
from: string,
to: string,
urgent = false
): Observable<Flight[]> {
private find(from: string, to: string, urgent = false): Observable<Flight[]> {
let url = [this.baseUrl, 'flight'].join('/');

if (urgent) {
Expand All @@ -75,5 +71,4 @@ export class FlightService implements DataService<Flight, FlightFilter> {
const url = [this.baseUrl, 'flight', flight.id].join('/');
return this.http.delete<void>(url);
}

}
2 changes: 1 addition & 1 deletion apps/demo/src/app/todo-storage-sync/synced-todo-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Loading

0 comments on commit 48f9d95

Please sign in to comment.