Skip to content

Commit

Permalink
feat(RouterStore): Added serializer for router state snapshot
Browse files Browse the repository at this point in the history
This adds a serializer that can be customized for returning the router state. By default, the entire RouterStateSnapshot is returned. A custom serializer can be provided to parse the snapshot into a more managable structure.

Closes #104, #97
  • Loading branch information
brandonroberts committed Aug 6, 2017
1 parent 7d23fdb commit 9a4a26b
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 18 deletions.
7 changes: 5 additions & 2 deletions docs/router-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Install @ngrx/router-store from npm:
During the navigation, before any guards or resolvers run, the router will dispatch a ROUTER_NAVIGATION action, which has the following signature:

```ts
export type RouterNavigationPayload = {
routerState: RouterStateSnapshot,
export type RouterNavigationPayload<T> = {
routerState: T,
event: RoutesRecognized
}
```
Expand Down Expand Up @@ -46,3 +46,6 @@ import { App } from './app.component';
})
export class AppModule { }
```

## API Documentation
- [Custom Router State Serializer](./api.md#custom-router-state-serializer)
46 changes: 46 additions & 0 deletions docs/router-store/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# API

## Custom Router State Serializer

During each navigation cycle, a `RouterNavigationAction` is dispatched with a snapshot of the state in its payload, the `RouterStateSnapshot`. The `RouterStateSnapshot` is a large complex structure, containing many pieces of information about the current state and what's rendered by the router. This can cause performance
issues when used with the Store Devtools. In most cases, you may only need a piece of information from the `RouterStateSnapshot`. In order to pair down the `RouterStateSnapshot` provided during navigation, you provide a custom serializer for the snapshot to only return what you need to be added to the payload and store.

To use the time-traveling debugging in the Devtools, you must return an object containing the `url` when using the `routerReducer`.

```ts
import { StoreModule } from '@ngrx/store';
import {
StoreRouterConnectingModule,
routerReducer,
RouterStateSerializer,
RouterStateSnapshotType
} from '@ngrx/router-store';

export interface RouterStateUrl {
url: string;
}

export class CustomSerializer implements RouterStateSerializer<RouterStateUrl> {
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
const url = routerState ? routerState.url : '';

// Only return an object including the URL
// instead of the entire snapshot
return { url };
}
}

@NgModule({
imports: [
StoreModule.forRoot({ routerReducer: routerReducer }),
RouterModule.forRoot([
// routes
]),
StoreRouterConnectingModule
],
providers: [
{ provide: RouterStateSerializer, useClass: CustomSerializer }
]
})
export class AppModule { }
```
14 changes: 13 additions & 1 deletion example-app/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { HttpModule } from '@angular/http';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { DBModule } from '@ngrx/db';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import {
StoreRouterConnectingModule,
RouterStateSerializer,
} from '@ngrx/router-store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

import { CoreModule } from './core/core.module';
Expand All @@ -17,6 +20,7 @@ import { AuthModule } from './auth/auth.module';
import { routes } from './routes';
import { reducers, metaReducers } from './reducers';
import { schema } from './db';
import { CustomRouterStateSerializer } from './shared/utils';

import { AppComponent } from './core/containers/app';
import { environment } from '../environments/environment';
Expand Down Expand Up @@ -74,6 +78,14 @@ import { environment } from '../environments/environment';

AuthModule.forRoot(),
],
providers: [
/**
* The `RouterStateSnapshot` provided by the `Router` is a large complex structure.
* A custom RouterStateSerializer is used to parse the `RouterStateSnapshot` provided
* by `@ngrx/router-store` to include only the desired pieces of the snapshot.
*/
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer },
],
bootstrap: [AppComponent],
})
export class AppModule {}
3 changes: 3 additions & 0 deletions example-app/app/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ActionReducer,
} from '@ngrx/store';
import { environment } from '../../environments/environment';
import * as fromRouter from '@ngrx/router-store';

/**
* Every reducer module's default export is the reducer function itself. In
Expand All @@ -21,6 +22,7 @@ import * as fromLayout from '../core/reducers/layout';
*/
export interface State {
layout: fromLayout.State;
routerReducer: fromRouter.RouterReducerState;
}

/**
Expand All @@ -30,6 +32,7 @@ export interface State {
*/
export const reducers: ActionReducerMap<State> = {
layout: fromLayout.reducer,
routerReducer: fromRouter.routerReducer,
};

// console.log all actions
Expand Down
24 changes: 24 additions & 0 deletions example-app/app/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RouterStateSerializer } from '@ngrx/router-store';
import { RouterStateSnapshot } from '@angular/router';

/**
* The RouterStateSerializer takes the current RouterStateSnapshot
* and returns any pertinent information needed. The snapshot contains
* all information about the state of the router at the given point in time.
* The entire snapshot is complex and not always needed. In this case, you only
* need the URL from the snapshot in the store. Other items could be
* returned such as route parameters, query parameters and static route data.
*/

export interface RouterStateUrl {
url: string;
}

export class CustomRouterStateSerializer
implements RouterStateSerializer<RouterStateUrl> {
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
const url = routerState ? routerState.url : '';

return { url };
}
}
57 changes: 54 additions & 3 deletions modules/router-store/spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { Component, Provider } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NavigationEnd, Router } from '@angular/router';
import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Store, StoreModule } from '@ngrx/store';
import {
Expand All @@ -10,6 +10,7 @@ import {
RouterAction,
routerReducer,
StoreRouterConnectingModule,
RouterStateSerializer,
} from '../src/index';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
Expand Down Expand Up @@ -315,11 +316,60 @@ describe('integration spec', () => {
]);
done();
});

it('should support a custom RouterStateSnapshot serializer ', done => {
const reducer = (state: any, action: RouterAction<any>) => {
const r = routerReducer(state, action);
return r && r.state
? { url: r.state.url, navigationId: r.navigationId }
: null;
};

class CustomSerializer implements RouterStateSerializer<{ url: string }> {
serialize(routerState: RouterStateSnapshot) {
const url = routerState ? `${routerState.url}-custom` : '';

return { url };
}
}

const providers = [
{ provide: RouterStateSerializer, useClass: CustomSerializer },
];

createTestModule({ reducers: { routerReducer, reducer }, providers });

const router = TestBed.get(Router);
const store = TestBed.get(Store);
const log = logOfRouterAndStore(router, store);

router
.navigateByUrl('/')
.then(() => {
log.splice(0);
return router.navigateByUrl('next');
})
.then(() => {
expect(log).toEqual([
{ type: 'router', event: 'NavigationStart', url: '/next' },
{ type: 'router', event: 'RoutesRecognized', url: '/next' },
{ type: 'store', state: { url: '/next-custom', navigationId: 2 } },
{ type: 'router', event: 'NavigationEnd', url: '/next' },
]);
log.splice(0);
done();
});
});
});
});

function createTestModule(
opts: { reducers?: any; canActivate?: Function; canLoad?: Function } = {}
opts: {
reducers?: any;
canActivate?: Function;
canLoad?: Function;
providers?: Provider[];
} = {}
) {
@Component({
selector: 'test-app',
Expand Down Expand Up @@ -361,6 +411,7 @@ function createTestModule(
provide: 'CanLoadNext',
useValue: opts.canLoad || (() => true),
},
opts.providers || [],
],
});

Expand Down
5 changes: 5 additions & 0 deletions modules/router-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export {
RouterNavigationPayload,
StoreRouterConnectingModule,
} from './router_store_module';

export {
RouterStateSerializer,
DefaultRouterStateSerializer,
} from './serializer';
40 changes: 28 additions & 12 deletions modules/router-store/src/router_store_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
} from '@angular/router';
import { Store } from '@ngrx/store';
import { of } from 'rxjs/observable/of';

import {
DefaultRouterStateSerializer,
RouterStateSerializer,
} from './serializer';
/**
* An action dispatched when the router navigates.
*/
Expand All @@ -17,17 +20,17 @@ export const ROUTER_NAVIGATION = 'ROUTER_NAVIGATION';
/**
* Payload of ROUTER_NAVIGATION.
*/
export type RouterNavigationPayload = {
routerState: RouterStateSnapshot;
export type RouterNavigationPayload<T> = {
routerState: T;
event: RoutesRecognized;
};

/**
* An action dispatched when the router navigates.
*/
export type RouterNavigationAction = {
export type RouterNavigationAction<T> = {
type: typeof ROUTER_NAVIGATION;
payload: RouterNavigationPayload;
payload: RouterNavigationPayload<T>;
};

/**
Expand Down Expand Up @@ -78,7 +81,7 @@ export type RouterErrorAction<T> = {
* An union type of router actions.
*/
export type RouterAction<T> =
| RouterNavigationAction
| RouterNavigationAction<T>
| RouterCancelAction<T>
| RouterErrorAction<T>;

Expand Down Expand Up @@ -133,7 +136,7 @@ export function routerReducer(
* declarations: [AppCmp, SimpleCmp],
* imports: [
* BrowserModule,
* StoreModule.provideStore(mapOfReducers),
* StoreModule.forRoot(mapOfReducers),
* RouterModule.forRoot([
* { path: '', component: SimpleCmp },
* { path: 'next', component: SimpleCmp }
Expand All @@ -146,16 +149,24 @@ export function routerReducer(
* }
* ```
*/
@NgModule({})
@NgModule({
providers: [
{ provide: RouterStateSerializer, useClass: DefaultRouterStateSerializer },
],
})
export class StoreRouterConnectingModule {
private routerState: RouterStateSnapshot | null = null;
private routerState: RouterStateSnapshot;
private storeState: any;
private lastRoutesRecognized: RoutesRecognized;

private dispatchTriggeredByRouter: boolean = false; // used only in dev mode in combination with routerReducer
private navigationTriggeredByDispatch: boolean = false; // used only in dev mode in combination with routerReducer

constructor(private store: Store<any>, private router: Router) {
constructor(
private store: Store<any>,
private router: Router,
private serializer: RouterStateSerializer<RouterStateSnapshot>
) {
this.setUpBeforePreactivationHook();
this.setUpStoreStateListener();
this.setUpStateRollbackEvents();
Expand All @@ -165,7 +176,7 @@ export class StoreRouterConnectingModule {
(<any>this.router).hooks.beforePreactivation = (
routerState: RouterStateSnapshot
) => {
this.routerState = routerState;
this.routerState = this.serializer.serialize(routerState);
if (this.shouldDispatchRouterNavigation())
this.dispatchRouterNavigation();
return of(true);
Expand Down Expand Up @@ -214,7 +225,12 @@ export class StoreRouterConnectingModule {
private dispatchRouterNavigation(): void {
this.dispatchRouterAction(ROUTER_NAVIGATION, {
routerState: this.routerState,
event: this.lastRoutesRecognized,
event: {
id: this.lastRoutesRecognized.id,
url: this.lastRoutesRecognized.url,
urlAfterRedirects: this.lastRoutesRecognized.urlAfterRedirects,
state: this.serializer.serialize(this.routerState),
} as RoutesRecognized,
});
}

Expand Down
13 changes: 13 additions & 0 deletions modules/router-store/src/serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { InjectionToken } from '@angular/core';
import { RouterStateSnapshot } from '@angular/router';

export abstract class RouterStateSerializer<T> {
abstract serialize(routerState: RouterStateSnapshot): T;
}

export class DefaultRouterStateSerializer
implements RouterStateSerializer<RouterStateSnapshot> {
serialize(routerState: RouterStateSnapshot) {
return routerState;
}
}

0 comments on commit 9a4a26b

Please sign in to comment.