|
1 | | -# Router Component Store |
| 1 | +# ngworkers Router Store |
2 | 2 |
|
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"> |
4 | 4 |
|
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`. |
6 | 6 |
|
7 | | -A strictly typed lightweight alternative to NgRx Router Store (`@ngrx/router-store`) and `ActivatedRoute`. |
| 7 | +## Packages |
8 | 8 |
|
9 | | -## Compatibility |
10 | | - |
11 | | -Required peer dependencies: |
| 9 | +This repository contains two main packages: |
12 | 10 |
|
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) |
17 | 12 |
|
18 | | -Published with partial Ivy compilation. |
| 13 | +A Component Store-based solution using observables: |
19 | 14 |
|
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 |
21 | 18 |
|
22 | | -## Guiding principles |
| 19 | +```typescript |
| 20 | +// Observable-based API |
| 21 | +export class HeroService { |
| 22 | + #routerStore = inject(RouterStore); |
23 | 23 |
|
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 | +``` |
25 | 28 |
|
26 | | -The following principles guide the development of Router Component Store. |
| 29 | +### [`@ngworker/router-signal-store`](packages/router-signal-store/README.md) |
27 | 30 |
|
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: |
32 | 32 |
|
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 |
34 | 36 |
|
35 | | -### RouterStore |
| 37 | +```typescript |
| 38 | +// Signal-based API |
| 39 | +export class HeroService { |
| 40 | + #routerSignalStore = inject(RouterSignalStore); |
36 | 41 |
|
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 | +``` |
38 | 46 |
|
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 |
52 | 48 |
|
53 | | -A `RouterStore` service is provided by using either `provideGlobalRouterStore`or `provideLocalRouterStore`. |
| 49 | +Both packages provide: |
54 | 50 |
|
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 |
56 | 57 |
|
57 | | -It emits values similar to `@ngrx/router-store` selectors. A comparison is in the documentation. |
| 58 | +## Compatibility |
58 | 59 |
|
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. |
60 | 61 |
|
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. |
62 | 63 |
|
63 | | -#### Global router store |
| 64 | +The packages require the same peer dependencies as their corresponding Angular and NgRx packages. |
64 | 65 |
|
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 |
66 | 67 |
|
67 | | -Use a global router store instead of NgRx Router Store. |
| 68 | +### Observable-based (Router Component Store) |
68 | 69 |
|
69 | | -Providing in a standalone Angular application: |
| 70 | +```bash |
| 71 | +npm install @ngworker/router-component-store |
| 72 | +``` |
70 | 73 |
|
71 | 74 | ```typescript |
72 | | -// main.ts |
73 | | -// (...) |
74 | 75 | import { provideGlobalRouterStore } from '@ngworker/router-component-store'; |
75 | 76 |
|
76 | 77 | bootstrapApplication(AppComponent, { |
77 | 78 | providers: [provideGlobalRouterStore()], |
78 | | -}).catch((error) => console.error(error)); |
| 79 | +}); |
79 | 80 | ``` |
80 | 81 |
|
81 | | -Providing in a classic Angular application: |
| 82 | +### Signal-based (Router Signal Store) |
| 83 | + |
| 84 | +```bash |
| 85 | +npm install @ngworker/router-signal-store |
| 86 | +``` |
82 | 87 |
|
83 | 88 | ```typescript |
84 | | -// app.module.ts |
85 | | -// (...) |
86 | | -import { provideGlobalRouterStore } from '@ngworker/router-component-store'; |
| 89 | +import { provideGlobalRouterSignalStore } from '@ngworker/router-signal-store'; |
87 | 90 |
|
88 | | -@NgModule({ |
89 | | - // (...) |
90 | | - providers: [provideGlobalRouterStore()], |
91 | | -}) |
92 | | -export class AppModule {} |
| 91 | +bootstrapApplication(AppComponent, { |
| 92 | + providers: [provideGlobalRouterSignalStore()], |
| 93 | +}); |
93 | 94 | ``` |
94 | 95 |
|
95 | | -Usage in service: |
| 96 | +## API comparison |
96 | 97 |
|
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_ | |
101 | 108 |
|
102 | | -@Injectable({ |
103 | | - providedIn: 'root', |
104 | | -}) |
105 | | -export class HeroService { |
106 | | - #routerStore = inject(RouterStore); |
| 109 | +## Usage patterns |
107 | 110 |
|
108 | | - activeHeroId$: Observable<string | undefined> = this.#routerStore.selectRouteParam('id'); |
109 | | -} |
110 | | -``` |
| 111 | +### Global usage (application-wide) |
111 | 112 |
|
112 | | -Usage in component: |
| 113 | +Both packages support global router stores for application-wide router state access: |
113 | 114 |
|
114 | 115 | ```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 { |
123 | 119 | #routerStore = inject(RouterStore); |
| 120 | + currentPath$ = this.#routerStore.url$; |
| 121 | +} |
124 | 122 |
|
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; |
126 | 128 | } |
127 | 129 | ``` |
128 | 130 |
|
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) |
136 | 132 |
|
137 | | -Usage in component: |
| 133 | +Both packages support local router stores for component-specific router state: |
138 | 134 |
|
139 | 135 | ```typescript |
140 | | -// hero-detail.component.ts |
141 | | -// (...) |
142 | | -import { provideLocalRouterStore, RouterStore } from '@ngworker/router-component-store'; |
143 | | - |
| 136 | +// Component Store |
144 | 137 | @Component({ |
145 | | - // (...) |
146 | 138 | providers: [provideLocalRouterStore()], |
147 | 139 | }) |
148 | | -export class HeroDetailComponent { |
| 140 | +export class ProductComponent { |
149 | 141 | #routerStore = inject(RouterStore); |
| 142 | + productId$ = this.#routerStore.selectRouteParam('id'); |
| 143 | +} |
150 | 144 |
|
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'); |
152 | 152 | } |
153 | 153 | ``` |
154 | 154 |
|
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 |
158 | 156 |
|
159 | | -#### MinimalActivatedRouteSnapshot |
| 157 | +This repository uses: |
160 | 158 |
|
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 |
162 | 164 |
|
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 |
175 | 166 |
|
176 | | -#### StrictQueryParams |
| 167 | +```bash |
| 168 | +# Install dependencies |
| 169 | +yarn install |
177 | 170 |
|
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 |
179 | 173 |
|
180 | | -`StrictQueryParams` has the following signature. |
| 174 | +# Run linting |
| 175 | +yarn lint |
181 | 176 |
|
182 | | -```typescript |
183 | | -export type StrictQueryParams = { |
184 | | - readonly [key: string]: string | readonly string[] | undefined; |
185 | | -}; |
186 | | -``` |
| 177 | +# Build packages |
| 178 | +yarn build |
187 | 179 |
|
188 | | -#### StrictRouteData |
| 180 | +# Perform dead code analysis |
| 181 | +yarn knip |
189 | 182 |
|
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 |
191 | 185 |
|
192 | | -`StrictRouteData` has the following signature. |
| 186 | +# Check formatting |
| 187 | +yarn format:check |
193 | 188 |
|
194 | | -```typescript |
195 | | -export type StrictRouteData = { |
196 | | - readonly [key: string]: unknown; |
197 | | -}; |
| 189 | +# Format code |
| 190 | +yarn format |
198 | 191 | ``` |
199 | 192 |
|
200 | | -#### StrictRouteParams |
| 193 | +## Documentation |
201 | 194 |
|
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/) |
203 | 198 |
|
204 | | -`StrictRouteParams` has the following signature. |
| 199 | +## Contributing |
205 | 200 |
|
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) |
0 commit comments