Skip to content

Commit 4ad9a94

Browse files
koumatsumototimdeschryver
authored andcommitted
feat(router-store): update stateKey definition to take a string or selector
Closes #1300
1 parent df8fc60 commit 4ad9a94

File tree

5 files changed

+282
-69
lines changed

5 files changed

+282
-69
lines changed

modules/router-store/spec/integration.spec.ts

Lines changed: 82 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Provider, Injectable, ErrorHandler } from '@angular/core';
1+
import { Injectable, ErrorHandler } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
33
import {
44
NavigationEnd,
@@ -7,8 +7,7 @@ import {
77
NavigationCancel,
88
NavigationError,
99
} from '@angular/router';
10-
import { RouterTestingModule } from '@angular/router/testing';
11-
import { Store, StoreModule, ScannedActionsSubject } from '@ngrx/store';
10+
import { Store, ScannedActionsSubject } from '@ngrx/store';
1211
import { filter, first, mapTo, take } from 'rxjs/operators';
1312

1413
import {
@@ -22,9 +21,9 @@ import {
2221
routerReducer,
2322
RouterReducerState,
2423
RouterStateSerializer,
25-
StoreRouterConfig,
26-
StoreRouterConnectingModule,
24+
StateKeyOrSelector,
2725
} from '../src';
26+
import { createTestModule } from './utils';
2827

2928
describe('integration spec', () => {
3029
it('should work', (done: any) => {
@@ -677,12 +676,76 @@ describe('integration spec', () => {
677676
};
678677

679678
createTestModule({
680-
reducers: { reducer },
679+
reducers: { 'router-reducer': reducer },
681680
config: { stateKey: 'router-reducer' },
682681
});
683682

684683
const router: Router = TestBed.get(Router);
685-
const log = logOfRouterAndActionsAndStore();
684+
const log = logOfRouterAndActionsAndStore({ stateKey: 'router-reducer' });
685+
686+
router
687+
.navigateByUrl('/')
688+
.then(() => {
689+
expect(log).toEqual([
690+
{ type: 'store', state: '' }, // init event. has nothing to do with the router
691+
{ type: 'store', state: '' }, // ROUTER_REQUEST event in the store
692+
{ type: 'action', action: ROUTER_REQUEST },
693+
{ type: 'router', event: 'NavigationStart', url: '/' },
694+
{ type: 'store', state: '/' }, // ROUTER_NAVIGATION event in the store
695+
{ type: 'action', action: ROUTER_NAVIGATION },
696+
{ type: 'router', event: 'RoutesRecognized', url: '/' },
697+
{ type: 'router', event: 'GuardsCheckStart', url: '/' },
698+
{ type: 'router', event: 'GuardsCheckEnd', url: '/' },
699+
{ type: 'router', event: 'ResolveStart', url: '/' },
700+
{ type: 'router', event: 'ResolveEnd', url: '/' },
701+
{ type: 'store', state: '/' }, // ROUTER_NAVIGATED event in the store
702+
{ type: 'action', action: ROUTER_NAVIGATED },
703+
{ type: 'router', event: 'NavigationEnd', url: '/' },
704+
]);
705+
})
706+
.then(() => {
707+
log.splice(0);
708+
return router.navigateByUrl('next');
709+
})
710+
.then(() => {
711+
expect(log).toEqual([
712+
{ type: 'store', state: '/' },
713+
{ type: 'action', action: ROUTER_REQUEST },
714+
{ type: 'router', event: 'NavigationStart', url: '/next' },
715+
{ type: 'store', state: '/next' },
716+
{ type: 'action', action: ROUTER_NAVIGATION },
717+
{ type: 'router', event: 'RoutesRecognized', url: '/next' },
718+
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
719+
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
720+
{ type: 'router', event: 'ResolveStart', url: '/next' },
721+
{ type: 'router', event: 'ResolveEnd', url: '/next' },
722+
{ type: 'store', state: '/next' },
723+
{ type: 'action', action: ROUTER_NAVIGATED },
724+
{ type: 'router', event: 'NavigationEnd', url: '/next' },
725+
]);
726+
727+
done();
728+
});
729+
});
730+
731+
it('should work when defining state selector', (done: any) => {
732+
const reducer = (state: string = '', action: RouterAction<any>) => {
733+
if (action.type === ROUTER_NAVIGATION) {
734+
return action.payload.routerState.url.toString();
735+
} else {
736+
return state;
737+
}
738+
};
739+
740+
createTestModule({
741+
reducers: { routerReducer: reducer },
742+
config: { stateKey: (state: any) => state.routerReducer },
743+
});
744+
745+
const router: Router = TestBed.get(Router);
746+
const log = logOfRouterAndActionsAndStore({
747+
stateKey: (state: any) => state.routerReducer,
748+
});
686749

687750
router
688751
.navigateByUrl('/')
@@ -825,62 +888,6 @@ describe('integration spec', () => {
825888
});
826889
});
827890

828-
function createTestModule(
829-
opts: {
830-
reducers?: any;
831-
canActivate?: Function;
832-
canLoad?: Function;
833-
providers?: Provider[];
834-
config?: StoreRouterConfig;
835-
} = {}
836-
) {
837-
@Component({
838-
selector: 'test-app',
839-
template: '<router-outlet></router-outlet>',
840-
})
841-
class AppCmp {}
842-
843-
@Component({
844-
selector: 'pagea-cmp',
845-
template: 'pagea-cmp',
846-
})
847-
class SimpleCmp {}
848-
849-
TestBed.configureTestingModule({
850-
declarations: [AppCmp, SimpleCmp],
851-
imports: [
852-
StoreModule.forRoot(opts.reducers),
853-
RouterTestingModule.withRoutes([
854-
{ path: '', component: SimpleCmp },
855-
{
856-
path: 'next',
857-
component: SimpleCmp,
858-
canActivate: ['CanActivateNext'],
859-
},
860-
{
861-
path: 'load',
862-
loadChildren: 'test',
863-
canLoad: ['CanLoadNext'],
864-
},
865-
]),
866-
StoreRouterConnectingModule.forRoot(opts.config),
867-
],
868-
providers: [
869-
{
870-
provide: 'CanActivateNext',
871-
useValue: opts.canActivate || (() => true),
872-
},
873-
{
874-
provide: 'CanLoadNext',
875-
useValue: opts.canLoad || (() => true),
876-
},
877-
opts.providers || [],
878-
],
879-
});
880-
881-
TestBed.createComponent(AppCmp);
882-
}
883-
884891
function waitForNavigation(router: Router, event: any = NavigationEnd) {
885892
return router.events
886893
.pipe(
@@ -897,7 +904,11 @@ function waitForNavigation(router: Router, event: any = NavigationEnd) {
897904
* Example: router event is fired -> store is updated -> store log appears before router log
898905
* Also, actions$ always fires the next action AFTER the store is updated
899906
*/
900-
function logOfRouterAndActionsAndStore(): any[] {
907+
function logOfRouterAndActionsAndStore(
908+
options: { stateKey: StateKeyOrSelector } = {
909+
stateKey: 'reducer',
910+
}
911+
): any[] {
901912
const router: Router = TestBed.get(Router);
902913
const store: Store<any> = TestBed.get(Store);
903914
// Not using effects' Actions to avoid @ngrx/effects dependency
@@ -915,6 +926,12 @@ function logOfRouterAndActionsAndStore(): any[] {
915926
actions$.subscribe(action =>
916927
log.push({ type: 'action', action: action.type })
917928
);
918-
store.subscribe(store => log.push({ type: 'store', state: store.reducer }));
929+
store.subscribe(store => {
930+
if (typeof options.stateKey === 'function') {
931+
log.push({ type: 'store', state: options.stateKey(store) });
932+
} else {
933+
log.push({ type: 'store', state: store[options.stateKey] });
934+
}
935+
});
919936
return log;
920937
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { Router } from '@angular/router';
3+
import {
4+
routerReducer,
5+
RouterReducerState,
6+
StoreRouterConnectingModule,
7+
} from '@ngrx/router-store';
8+
import { select, Store } from '@ngrx/store';
9+
import { withLatestFrom } from 'rxjs/operators';
10+
11+
import { createTestModule } from './utils';
12+
13+
describe('Router Store Module', () => {
14+
describe('with defining state key', () => {
15+
const customStateKey = 'router-reducer';
16+
let storeRouterConnectingModule: StoreRouterConnectingModule;
17+
let store: Store<State>;
18+
let router: Router;
19+
20+
interface State {
21+
[customStateKey]: RouterReducerState;
22+
}
23+
24+
beforeEach(() => {
25+
createTestModule({
26+
reducers: {
27+
[customStateKey]: routerReducer,
28+
},
29+
config: {
30+
stateKey: customStateKey,
31+
},
32+
});
33+
34+
store = TestBed.get(Store);
35+
router = TestBed.get(Router);
36+
storeRouterConnectingModule = TestBed.get(StoreRouterConnectingModule);
37+
});
38+
39+
it('should have custom state key as own property', () => {
40+
expect((<any>storeRouterConnectingModule).stateKey).toBe(customStateKey);
41+
});
42+
43+
it('should call navigateIfNeeded with args selected by custom state key', (done: any) => {
44+
let logs: any[] = [];
45+
store
46+
.pipe(
47+
select(customStateKey),
48+
withLatestFrom(store)
49+
)
50+
.subscribe(([routerStoreState, storeState]) => {
51+
logs.push([routerStoreState, storeState]);
52+
});
53+
54+
spyOn(storeRouterConnectingModule, 'navigateIfNeeded').and.callThrough();
55+
logs = [];
56+
57+
// this dispatches `@ngrx/router-store/navigation` action
58+
// and store emits its payload.
59+
router.navigateByUrl('/').then(() => {
60+
const actual = (<any>(
61+
storeRouterConnectingModule
62+
)).navigateIfNeeded.calls.allArgs();
63+
64+
expect(actual.length).toBe(1);
65+
expect(actual[0]).toEqual(logs[0]);
66+
done();
67+
});
68+
});
69+
});
70+
71+
describe('with defining state selector', () => {
72+
const customStateKey = 'routerReducer';
73+
const customStateSelector = (state: State) => state.routerReducer;
74+
75+
let storeRouterConnectingModule: StoreRouterConnectingModule;
76+
let store: Store<State>;
77+
let router: Router;
78+
79+
interface State {
80+
[customStateKey]: RouterReducerState;
81+
}
82+
83+
beforeEach(() => {
84+
createTestModule({
85+
reducers: {
86+
[customStateKey]: routerReducer,
87+
},
88+
config: {
89+
stateKey: customStateSelector,
90+
},
91+
});
92+
93+
store = TestBed.get(Store);
94+
router = TestBed.get(Router);
95+
storeRouterConnectingModule = TestBed.get(StoreRouterConnectingModule);
96+
});
97+
98+
it('should have same state selector as own property', () => {
99+
expect((<any>storeRouterConnectingModule).stateKey).toBe(
100+
customStateSelector
101+
);
102+
});
103+
104+
it('should call navigateIfNeeded with args selected by custom state selector', (done: any) => {
105+
let logs: any[] = [];
106+
store
107+
.pipe(
108+
select(customStateSelector),
109+
withLatestFrom(store)
110+
)
111+
.subscribe(([routerStoreState, storeState]) => {
112+
logs.push([routerStoreState, storeState]);
113+
});
114+
115+
spyOn(storeRouterConnectingModule, 'navigateIfNeeded').and.callThrough();
116+
logs = [];
117+
118+
// this dispatches `@ngrx/router-store/navigation` action
119+
// and store emits its payload.
120+
router.navigateByUrl('/').then(() => {
121+
const actual = (<any>(
122+
storeRouterConnectingModule
123+
)).navigateIfNeeded.calls.allArgs();
124+
125+
expect(actual.length).toBe(1);
126+
expect(actual[0]).toEqual(logs[0]);
127+
done();
128+
});
129+
});
130+
});
131+
});

modules/router-store/spec/utils.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Component, Provider } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { RouterTestingModule } from '@angular/router/testing';
4+
import { StoreModule } from '@ngrx/store';
5+
6+
import { StoreRouterConfig, StoreRouterConnectingModule } from '../src';
7+
8+
export function createTestModule(
9+
opts: {
10+
reducers?: any;
11+
canActivate?: Function;
12+
canLoad?: Function;
13+
providers?: Provider[];
14+
config?: StoreRouterConfig;
15+
} = {}
16+
) {
17+
@Component({
18+
selector: 'test-app',
19+
template: '<router-outlet></router-outlet>',
20+
})
21+
class AppCmp {}
22+
23+
@Component({
24+
selector: 'page-cmp',
25+
template: 'page-cmp',
26+
})
27+
class SimpleCmp {}
28+
29+
TestBed.configureTestingModule({
30+
declarations: [AppCmp, SimpleCmp],
31+
imports: [
32+
StoreModule.forRoot(opts.reducers),
33+
RouterTestingModule.withRoutes([
34+
{ path: '', component: SimpleCmp },
35+
{
36+
path: 'next',
37+
component: SimpleCmp,
38+
canActivate: ['CanActivateNext'],
39+
},
40+
{
41+
path: 'load',
42+
loadChildren: 'test',
43+
canLoad: ['CanLoadNext'],
44+
},
45+
]),
46+
StoreRouterConnectingModule.forRoot(opts.config),
47+
],
48+
providers: [
49+
{
50+
provide: 'CanActivateNext',
51+
useValue: opts.canActivate || (() => true),
52+
},
53+
{
54+
provide: 'CanLoadNext',
55+
useValue: opts.canLoad || (() => true),
56+
},
57+
opts.providers || [],
58+
],
59+
});
60+
61+
TestBed.createComponent(AppCmp);
62+
}

modules/router-store/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
} from './actions';
1919
export { routerReducer, RouterReducerState } from './reducer';
2020
export {
21+
StateKeyOrSelector,
2122
StoreRouterConnectingModule,
2223
StoreRouterConfig,
2324
NavigationActionTiming,

0 commit comments

Comments
 (0)