Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RouterStore): Added serializer for router state snapshot #188

Merged
merged 1 commit into from
Aug 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;

// 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;

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.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;
}
}