Skip to content

Commit 964c916

Browse files
authored
feat: create @ngworker/router-signal-store package using NgRx Signals (#356)
2 parents 2eeb933 + 7653698 commit 964c916

File tree

72 files changed

+5683
-232
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+5683
-232
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ jobs:
9797

9898
- name: Build
9999
run: yarn build
100-
- name: Bundle readme
101-
run: cp README.md dist/packages/router-component-store/
102100

103-
- name: '[Merge] Upload package bundle'
101+
- name: '[Merge] Upload package bundles'
104102
if: ${{ env.is-merge-to-main == 'true' }}
105103
uses: actions/upload-artifact@v4
106104
with:
107105
name: ngworker-router-component-store
108-
path: dist/packages/router-component-store
106+
path: |
107+
dist/packages/router-component-store
108+
dist/packages/router-signal-store
109109
if-no-files-found: error
110110
retention-days: 7

README.md

Lines changed: 130 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,210 +1,205 @@
1-
# Router Component Store
1+
# ngworkers Router Store
22

3-
[`@ngworker/router-component-store`](https://www.npmjs.com/package/@ngworker/router-component-store)
3+
<img src="logo.png" alt="Router Component Store" height="384px">
44

5-
<img src="https://github.com/ngworker/router-component-store/blob/main/logo.png" alt="Router Component Store" height="384px">
5+
A monorepo containing Angular router state management packages that provide strictly typed, lightweight alternatives to NgRx Router Store (`@ngrx/router-store`) and `ActivatedRoute`.
66

7-
A strictly typed lightweight alternative to NgRx Router Store (`@ngrx/router-store`) and `ActivatedRoute`.
7+
## Packages
88

9-
## Compatibility
10-
11-
Required peer dependencies:
9+
This repository contains two main packages:
1210

13-
- Angular 17.x
14-
- NgRx Component Store 17.x
15-
- RxJS >=7.5
16-
- TypeScript >=5.2
11+
### [`@ngworker/router-component-store`](packages/router-component-store/README.md)
1712

18-
Published with partial Ivy compilation.
13+
A Component Store-based solution using observables:
1914

20-
Find additional documentation in the [github.com/ngworker/router-component-store/docs](https://github.com/ngworker/router-component-store/tree/main/docs) directory.
15+
- **Package**: [`@ngworker/router-component-store`](https://www.npmjs.com/package/@ngworker/router-component-store)
16+
- **Technology**: NgRx Component Store (`@ngrx/component-store`) with RxJS observables
17+
- **Use Case**: When you prefer observable-based reactivity patterns
2118

22-
## Guiding principles
19+
```typescript
20+
// Observable-based API
21+
export class HeroService {
22+
#routerStore = inject(RouterStore);
2323

24-
Router Component Store is meant as a lightweight alternative to NgRx Router Store that additionaly can be used as a replacement for `ActivatedRoute` at any route level.
24+
url$ = this.#routerStore.url$; // Observable<string>
25+
searchTerm$ = this.#routerStore.selectQueryParam('q'); // Observable<string | undefined>
26+
}
27+
```
2528

26-
The following principles guide the development of Router Component Store.
29+
### [`@ngworker/router-signal-store`](packages/router-signal-store/README.md)
2730

28-
- The global router store closely matches NgRx Router Store selectors
29-
- Local router stores closely match `ActivatedRoute` observable properties
30-
- Router state is immutable and serializable
31-
- The API is strictly and strongly typed
31+
A Signal Store-based solution using Angular Signals:
3232

33-
## API
33+
- **Package**: [`@ngworker/router-signal-store`](https://www.npmjs.com/package/@ngworker/router-signal-store)
34+
- **Technology**: NgRx Signals (`@ngrx/signals`) with Angular Signals
35+
- **Use Case**: When you prefer signal-based reactivity patterns
3436

35-
### RouterStore
37+
```typescript
38+
// Signal-based API
39+
export class HeroService {
40+
#routerSignalStore = inject(RouterSignalStore);
3641

37-
A `RouterStore` service has the following public properties.
42+
url = this.#routerSignalStore.url; // Signal<string>
43+
searchTerm = this.#routerSignalStore.selectQueryParam('q'); // Signal<string | undefined>
44+
}
45+
```
3846

39-
| API | Description |
40-
| --------------------------------------------------------------------------------------- | --------------------------------------------------------- |
41-
| `currentRoute$: Observable<MinimalActivatedRouteSnapshot>` | Select the current route. |
42-
| `fragment$: Observable<string \| null>` | Select the current route fragment. |
43-
| `queryParams$: Observable<StrictQueryParams>` | Select the current route query parameters. |
44-
| `routeData$: Observable<StrictRouteData>` | Select the current route data. |
45-
| `routeParams$: Observable<StrictRouteParams>` | Select the current route parameters. |
46-
| `title$: Observable<string \| undefined>` | Select the resolved route title. |
47-
| `url$: Observable<string>` | Select the current URL. |
48-
| `selectQueryParam(param: string): Observable<string \| readonly string[] \| undefined>` | Select the specified query parameter. |
49-
| `selectRouteDataParam(key: string): Observable<unknown>` | Select the specified route data. |
50-
| `selectRouteParam(param: string): Observable<string \| undefined>` | Select the specified route parameter. |
51-
| `selectRouterEvents(...acceptedRouterEvents: RouterEvent[]): Observable<RouterEvent>` | Select router events of the specified router event types. |
47+
## Common features
5248

53-
A `RouterStore` service is provided by using either `provideGlobalRouterStore`or `provideLocalRouterStore`.
49+
Both packages provide:
5450

55-
The _global_ `RouterStore` service is provided in a root environment injector and is never destroyed but can be injected in any injection context.
51+
**Strictly Typed**: Full TypeScript support with strict typing
52+
**Lightweight**: Minimal bundle size impact
53+
**Drop-in Replacement**: For NgRx Router Store and `ActivatedRoute`
54+
**Global & Local Stores**: Application-wide and component-level usage
55+
**Immutable State**: Router state is immutable and serializable
56+
**Memory Safe**: Proper subscription management and cleanup
5657

57-
It emits values similar to `@ngrx/router-store` selectors. A comparison is in the documentation.
58+
## Compatibility
5859

59-
A _local_ `RouterStore` requires a component-level provider, follows the lifecycle of that component, and can be injected in declarables as well as other component-level services.
60+
Both packages follow Angular's recommended release pattern for Angular libraries. One major package version for every major Angular release.
6061

61-
It emits values similar to `ActivatedRoute`. A comparison is in the documentation.
62+
Features released in minor versions of Angular and NgRx packages are not used until the following major version package release to ensure compatibility across minor versions.
6263

63-
#### Global router store
64+
The packages require the same peer dependencies as their corresponding Angular and NgRx packages.
6465

65-
An application-wide router store that can be injected in any injection context. Use `provideGlobalRouterStore` to provide it in a root environment injector.
66+
## Quick start
6667

67-
Use a global router store instead of NgRx Router Store.
68+
### Observable-based (Router Component Store)
6869

69-
Providing in a standalone Angular application:
70+
```bash
71+
npm install @ngworker/router-component-store
72+
```
7073

7174
```typescript
72-
// main.ts
73-
// (...)
7475
import { provideGlobalRouterStore } from '@ngworker/router-component-store';
7576

7677
bootstrapApplication(AppComponent, {
7778
providers: [provideGlobalRouterStore()],
78-
}).catch((error) => console.error(error));
79+
});
7980
```
8081

81-
Providing in a classic Angular application:
82+
### Signal-based (Router Signal Store)
83+
84+
```bash
85+
npm install @ngworker/router-signal-store
86+
```
8287

8388
```typescript
84-
// app.module.ts
85-
// (...)
86-
import { provideGlobalRouterStore } from '@ngworker/router-component-store';
89+
import { provideGlobalRouterSignalStore } from '@ngworker/router-signal-store';
8790

88-
@NgModule({
89-
// (...)
90-
providers: [provideGlobalRouterStore()],
91-
})
92-
export class AppModule {}
91+
bootstrapApplication(AppComponent, {
92+
providers: [provideGlobalRouterSignalStore()],
93+
});
9394
```
9495

95-
Usage in service:
96+
## API comparison
9697

97-
```typescript
98-
// hero.service.ts
99-
// (...)
100-
import { RouterStore } from '@ngworker/router-component-store';
98+
| Feature | Router Component Store | Router Signal Store |
99+
| ---------------- | ------------------------------------------ | ------------------------------------------ |
100+
| Current Route | `currentRoute$: Observable<...>` | `currentRoute: Signal<...>` |
101+
| Query Parameters | `queryParams$: Observable<...>` | `queryParams: Signal<...>` |
102+
| Route Parameters | `routeParams$: Observable<...>` | `routeParams: Signal<...>` |
103+
| Route Data | `routeData$: Observable<...>` | `routeData: Signal<...>` |
104+
| URL | `url$: Observable<string>` | `url: Signal<string>` |
105+
| Fragment | `fragment$: Observable<...>` | `fragment: Signal<...>` |
106+
| Title | `title$: Observable<...>` | `title: Signal<...>` |
107+
| Router events | `selectRouterEvents(...): Observable<...>` | _Not included to avoid an RxJS dependency_ |
101108

102-
@Injectable({
103-
providedIn: 'root',
104-
})
105-
export class HeroService {
106-
#routerStore = inject(RouterStore);
109+
## Usage patterns
107110

108-
activeHeroId$: Observable<string | undefined> = this.#routerStore.selectRouteParam('id');
109-
}
110-
```
111+
### Global usage (application-wide)
111112

112-
Usage in component:
113+
Both packages support global router stores for application-wide router state access:
113114

114115
```typescript
115-
// hero-detail.component.ts
116-
// (...)
117-
import { RouterStore } from '@ngworker/router-component-store';
118-
119-
@Component({
120-
// (...)
121-
})
122-
export class HeroDetailComponent {
116+
// Component Store
117+
@Injectable({ providedIn: 'root' })
118+
export class NavigationService {
123119
#routerStore = inject(RouterStore);
120+
currentPath$ = this.#routerStore.url$;
121+
}
124122

125-
heroId$: Observable<string | undefined> = this.#routerStore.selectRouteParam('id');
123+
// Signal Store
124+
@Injectable({ providedIn: 'root' })
125+
export class NavigationService {
126+
#routerSignalStore = inject(RouterSignalStore);
127+
currentPath = this.#routerSignalStore.url;
126128
}
127129
```
128130

129-
#### Local router store
130-
131-
A component-level router store. Can be injected in any directive, component,
132-
pipe, or component-level service. Explicitly provided in a component sub-tree
133-
using `Component.providers` or `Component.viewProviders`.
134-
135-
Use a local router store instead of `ActivatedRoute`.
131+
### Local usage (component-level)
136132

137-
Usage in component:
133+
Both packages support local router stores for component-specific router state:
138134

139135
```typescript
140-
// hero-detail.component.ts
141-
// (...)
142-
import { provideLocalRouterStore, RouterStore } from '@ngworker/router-component-store';
143-
136+
// Component Store
144137
@Component({
145-
// (...)
146138
providers: [provideLocalRouterStore()],
147139
})
148-
export class HeroDetailComponent {
140+
export class ProductComponent {
149141
#routerStore = inject(RouterStore);
142+
productId$ = this.#routerStore.selectRouteParam('id');
143+
}
150144

151-
heroId$: Observable<string | undefined> = this.#routerStore.selectRouteParam('id');
145+
// Signal Store
146+
@Component({
147+
providers: [provideLocalRouterSignalStore()],
148+
})
149+
export class ProductComponent {
150+
#routerStore = inject(RouterSignalStore);
151+
productId = this.#routerStore.selectRouteParam('id');
152152
}
153153
```
154154

155-
### Serializable router state
156-
157-
Several of the Angular Router's types are recursive which means that they aren't serializable. The router stores exclusively use serializable types to support advanced state synchronization strategies.
155+
## Development
158156

159-
#### MinimalActivatedRouteSnapshot
157+
This repository uses:
160158

161-
The `MinimalActivatedRouteSnapshot` interface is used for the observable `RouterStore#currentRoute$` property. This interface is a serializable subset of the Angular Router's `ActivatedRouteSnapshot` class and has the following public properties.
159+
- **[Nx](https://nx.dev)** for monorepo, task, and CI management
160+
- **Angular**, **RxJS**, and **NgRx**
161+
- **TypeScript**
162+
- **Jest** for testing
163+
- **Prettier** and **ESLint** for code quality
162164

163-
| API | Description |
164-
| --------------------------------------------------- | ------------------------------------------------ |
165-
| `children: MinimalActivatedRouteSnapshot[]` | The children of this route in the route tree. |
166-
| `data: StrictRouteData` | The static and resolved data of this route. |
167-
| `firstChild: MinimalActivatedRouteSnapshot \| null` | The first child of this route in the route tree. |
168-
| `fragment: string \| null` | The URL fragment shared by all routes. |
169-
| `outlet: string` | The outlet name of the route. |
170-
| `params: StrictRouteParams` | The matrix parameters scoped to this route. |
171-
| `queryParams: StrictQueryParams` | The query parameters shared by all routes. |
172-
| `routeConfig: Route \| null` | The configuration used to match this route. |
173-
| `title: string \| undefined` | The resolved route title. |
174-
| `url: UrlSegment[]` | The URL segments matched by this route. |
165+
### Commands
175166

176-
#### StrictQueryParams
167+
```bash
168+
# Install dependencies
169+
yarn install
177170

178-
The `StrictQueryParams` type is used for query parameters in the `MinimalActivatedRouteSnapshot#queryParams` and `RouterStore#queryParams$` properties. It is a strictly typed version of the Angular Router's `Params` type where members are read-only and the `any` member type is replaced with `string | readonly string[] | undefined`.
171+
# Run tests
172+
yarn test
179173

180-
`StrictQueryParams` has the following signature.
174+
# Run linting
175+
yarn lint
181176

182-
```typescript
183-
export type StrictQueryParams = {
184-
readonly [key: string]: string | readonly string[] | undefined;
185-
};
186-
```
177+
# Build packages
178+
yarn build
187179

188-
#### StrictRouteData
180+
# Perform dead code analysis
181+
yarn knip
189182

190-
The `StrictRouteData` interface is used for the `MinimalActivatedRouteSnapshot#data` and `RouterStore#routeData$` properties. This interface is a serializable subset of the Angular Router's `Data` type. In particular, the `symbol` index in the Angular Router's `Data` type is removed. Additionally, the `any` member type is replaced with `unknown` for stricter typing.
183+
# Run all CI checks
184+
yarn ci
191185

192-
`StrictRouteData` has the following signature.
186+
# Check formatting
187+
yarn format:check
193188

194-
```typescript
195-
export type StrictRouteData = {
196-
readonly [key: string]: unknown;
197-
};
189+
# Format code
190+
yarn format
198191
```
199192

200-
#### StrictRouteParams
193+
## Documentation
201194

202-
The `StrictRouteParams` type is used for route parameters in the `MinimalActivatedRouteSnapshot#params` and `RouterStore#routeParams$` properties. It is a strictly typed version of the Angular Router's `Params` type where members are read-only and the `any` member type is replaced with `string | undefined`.
195+
- [Router Component Store package](packages/router-component-store/README.md)
196+
- [Router Signal Store package](packages/router-signal-store/README.md)
197+
- [Additional documentation](docs/)
203198

204-
`StrictRouteParams` has the following signature.
199+
## Contributing
205200

206-
```typescript
207-
export type StrictRouteParams = {
208-
readonly [key: string]: string | undefined;
209-
};
210-
```
201+
Contributions are welcome! Submit pull requests to the main branch.
202+
203+
## License
204+
205+
MIT © [ngworkers](https://github.com/ngworker)

docs/dependency-injection.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ To ensure injection of a local router store, use the `host` inject option.
4343
```typescript
4444
// crisis-detail.component.ts
4545
// (...)
46-
import {
47-
provideLocalRouterStore,
48-
RouterStore,
49-
} from '@ngworker/router-component-store';
46+
import { provideLocalRouterStore, RouterStore } from '@ngworker/router-component-store';
5047

5148
@Component({
5249
// (...)

0 commit comments

Comments
 (0)