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

Router resolve #524

Closed
wants to merge 11 commits into from
101 changes: 88 additions & 13 deletions modules/router-store/spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { StoreRouterConfig } from '../src/router_store_module';
import { Component, Provider } from '@angular/core';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/mapTo';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/toPromise';

import { Component, Injectable, Provider } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Store, StoreModule } from '@ngrx/store';

import {
ROUTER_CANCEL,
ROUTER_ERROR,
ROUTER_NAVIGATION,
ROUTER_RESOLVE_END,
RouterAction,
routerReducer,
StoreRouterConnectingModule,
RouterStateSerializer,
StoreRouterConnectingModule,
} from '../src/index';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/mapTo';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/toPromise';
import { of } from 'rxjs/observable/of';
import { StoreRouterConfig } from '../src/router_store_module';

describe('integration spec', () => {
it('should work', (done: any) => {
Expand Down Expand Up @@ -49,6 +51,9 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/' },
{ type: 'router', event: 'ResolveStart', url: '/' },

{ type: 'store', state: '/' }, // ROUTER_RESOLVE_END event in the store

{ type: 'router', event: 'ResolveEnd', url: '/' },

{ type: 'router', event: 'NavigationEnd', url: '/' },
Expand All @@ -68,6 +73,9 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },

{ type: 'store', state: '/next' }, // ROUTER_RESOLVE_END event in the store

{ type: 'router', event: 'ResolveEnd', url: '/next' },

{ type: 'router', event: 'NavigationEnd', url: '/next' },
Expand Down Expand Up @@ -264,7 +272,6 @@ describe('integration spec', () => {
routerReducerStates.push(state.routerReducer);
}
});

router
.navigateByUrl('/')
.then(() => {
Expand All @@ -281,12 +288,14 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },

{ type: 'store', state: { url: '/next', navigationId: 2 } },

{ type: 'router', event: 'ResolveEnd', url: '/next' },

{ type: 'router', event: 'NavigationEnd', url: '/next' },
]);
log.splice(0);

store.dispatch({
type: ROUTER_NAVIGATION,
payload: {
Expand All @@ -306,6 +315,9 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/' },
{ type: 'router', event: 'ResolveStart', url: '/' },

{ type: 'store', state: { url: '/', navigationId: 3 } },

{ type: 'router', event: 'ResolveEnd', url: '/' },

{ type: 'router', event: 'NavigationEnd', url: '/' },
Expand All @@ -316,8 +328,8 @@ describe('integration spec', () => {
store.dispatch({
type: ROUTER_NAVIGATION,
payload: {
routerState: routerReducerStates[1].state,
event: { id: routerReducerStates[1].navigationId },
routerState: routerReducerStates[2].state,
event: { id: routerReducerStates[2].navigationId },
},
});
return waitForNavigation(router);
Expand All @@ -332,6 +344,9 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },

{ type: 'store', state: { url: '/next', navigationId: 4 } },

{ type: 'router', event: 'ResolveEnd', url: '/next' },

{ type: 'router', event: 'NavigationEnd', url: '/next' },
Expand Down Expand Up @@ -457,6 +472,8 @@ describe('integration spec', () => {
{ type: 'store', state: undefined }, // after USER_EVENT
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },

{ type: 'store', state: undefined }, // after resolve start
{ type: 'router', event: 'ResolveEnd', url: '/next' },
{ type: 'router', event: 'NavigationEnd', url: '/next' },
]);
Expand Down Expand Up @@ -494,6 +511,7 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/' },
{ type: 'router', event: 'ResolveStart', url: '/' },
{ type: 'store', state: '/' }, //ROUTER_RESOLVE_END event in the store
{ type: 'router', event: 'ResolveEnd', url: '/' },
{ type: 'router', event: 'NavigationEnd', url: '/' },
]);
Expand All @@ -510,20 +528,66 @@ describe('integration spec', () => {
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },
{ type: 'store', state: '/next' }, //ROUTER_RESOLVE_END event in the store
{ type: 'router', event: 'ResolveEnd', url: '/next' },

{ type: 'router', event: 'NavigationEnd', url: '/next' },
]);

done();
});
});

it('dispatches event on resolve end event of routing phase', (done: any) => {
const reducer = (state: string = '', action: RouterAction<any>) => {
if (action.type === ROUTER_RESOLVE_END) {
const contact = (action.payload.event.state as any).root.children[0]
.data.contact;
return contact;
} else {
return state;
}
};
createTestModule({
reducers: { reducer },
});

const router: Router = TestBed.get(Router);
const store: Store<any> = 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: '' }, // after ROUTER_NAVIGATION

/* new Router Lifecycle in Angular 4.3 */
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },
{ type: 'store', state: { name: 'ngrx' } }, //ROUTER_RESOLVE_END event in the store
{ type: 'router', event: 'ResolveEnd', url: '/next' },

{ type: 'router', event: 'NavigationEnd', url: '/next' },
]);
done();
});
});
});

function createTestModule(
opts: {
reducers?: any;
canActivate?: Function;
canLoad?: Function;
resolvedContact?: string;
providers?: Provider[];
config?: StoreRouterConfig;
} = {}
Expand All @@ -550,6 +614,9 @@ function createTestModule(
path: 'next',
component: SimpleCmp,
canActivate: ['CanActivateNext'],
resolve: {
contact: ContactResolver,
},
},
{
path: 'load',
Expand All @@ -560,6 +627,7 @@ function createTestModule(
StoreRouterConnectingModule.forRoot(opts.config),
],
providers: [
ContactResolver,
{
provide: 'CanActivateNext',
useValue: opts.canActivate || (() => true),
Expand All @@ -575,6 +643,13 @@ function createTestModule(
TestBed.createComponent(AppCmp);
}

@Injectable()
export class ContactResolver {
resolve() {
return new Promise(r => r({ name: 'ngrx' }));
}
}

function waitForNavigation(router: Router): Promise<any> {
return router.events
.filter(e => e instanceof NavigationEnd)
Expand Down
1 change: 1 addition & 0 deletions modules/router-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
ROUTER_ERROR,
ROUTER_CANCEL,
ROUTER_NAVIGATION,
ROUTER_RESOLVE_END,
RouterNavigationAction,
RouterCancelAction,
RouterErrorAction,
Expand Down
36 changes: 34 additions & 2 deletions modules/router-store/src/router_store_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Router,
RouterStateSnapshot,
RoutesRecognized,
ResolveEnd,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { of } from 'rxjs/observable/of';
Expand Down Expand Up @@ -38,6 +39,26 @@ export type RouterNavigationAction<T = RouterStateSnapshot> = {
payload: RouterNavigationPayload<T>;
};

/**
* An action dispatched when the router resolve end.
*/
export const ROUTER_RESOLVE_END = 'ROUTER_RESOLVE_END';

/**
* Payload of ROUTER_RESOLVE_END.
*/
export type RouterResolveEndPayload<T> = {
routerState: T;
event: RoutesRecognized;
};

/**
* An action dispatched when the router resolve end.
*/
export type RouterResolveEndAction<T = RouterStateSnapshot> = {
type: typeof ROUTER_RESOLVE_END;
payload: RouterResolveEndPayload<T>;
};
/**
* An action dispatched when the router cancels navigation.
*/
Expand Down Expand Up @@ -87,6 +108,7 @@ export type RouterErrorAction<T, V = RouterStateSnapshot> = {
*/
export type RouterAction<T, V = RouterStateSnapshot> =
| RouterNavigationAction<T>
| RouterResolveEndAction<T>
| RouterCancelAction<T, V>
| RouterErrorAction<T, V>;

Expand All @@ -101,6 +123,7 @@ export function routerReducer<T = RouterStateSnapshot>(
): RouterReducerState<T> {
switch (action.type) {
case ROUTER_NAVIGATION:
case ROUTER_RESOLVE_END:
case ROUTER_ERROR:
case ROUTER_CANCEL:
return {
Expand Down Expand Up @@ -151,7 +174,7 @@ export type StoreRouterConfigFunction = () => StoreRouterConfig;
* event: RoutesRecognized
* }
* ```
*
* Router dispatches ROUTER_RESOLVE_END action when resolve phase ends.
* Either a reducer or an effect can be invoked in response to this action.
* If the invoked reducer throws, the navigation will be canceled.
*
Expand Down Expand Up @@ -221,7 +244,6 @@ export class StoreRouterConnectingModule {

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

private stateKey: string;

constructor(
Expand Down Expand Up @@ -285,6 +307,8 @@ export class StoreRouterConnectingModule {
this.dispatchRouterCancel(e);
} else if (e instanceof NavigationError) {
this.dispatchRouterError(e);
} else if (e instanceof ResolveEnd) {
this.dispatchRouterResolveEnd(e);
}
});
}
Expand All @@ -301,6 +325,14 @@ export class StoreRouterConnectingModule {
});
}

private dispatchRouterResolveEnd(event: ResolveEnd): void {
this.dispatchRouterAction(ROUTER_RESOLVE_END, {
routerState: this.routerState,
storeState: this.storeState,
event,
});
}

private dispatchRouterCancel(event: NavigationCancel): void {
this.dispatchRouterAction(ROUTER_CANCEL, {
routerState: this.routerState,
Expand Down
2 changes: 1 addition & 1 deletion modules/store-devtools/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionReducer } from '@ngrx/store';
import { ActionReducer, Action } from '@ngrx/store';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an unused import?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes and I am doing another checkin to remove those unused namespaces.
Below are required only. Basically this file does not need to be changed.
import { InjectionToken } from '@angular/core';
import { ActionReducer } from '@ngrx/store';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo any changes to this file. This is my mistake.

import { InjectionToken, Type } from '@angular/core';

export class StoreDevtoolsConfig {
Expand Down