From 3b4b8273a3ef35f68c80460aaadbdcc3a2ecf7f9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 29 Aug 2016 21:24:23 -0500 Subject: [PATCH] feat(actions): Added handling of an array for router actions (#12) The angular/router supports navigation provided as an array of commands. The router actions now support an array as well as a string for navigation Example: go(['/path', { page: 1 }], { query: { show: true } }) Will navigate to `/path;page=1?show=true` --- README.md | 60 +++++++++++++++++++++++++++++----------- spec/actions.spec.ts | 40 +++++++++++++++++++++------ spec/connect.spec.ts | 66 ++++++++++++++++++++++++++++++++------------ src/actions.ts | 8 +++--- src/connect.ts | 10 +++---- tests.ts | 26 ++++++++--------- 6 files changed, 147 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index f368e05..be4c46c 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,15 @@ ### Bindings to connect angular/router to ngrx/store -### Setup +### Installation -1. Use npm to install the bindings: ``` npm install @ngrx/router-store --save ``` -2. Use the `routerReducer` when providing the `StoreModule.provideStore` and add the `RouterStoreModule.connectRouter` to the `@NgModule.imports`: +### Setup + +1. Use the `routerReducer` when providing the `StoreModule.provideStore` and add the `RouterStoreModule.connectRouter` to the `@NgModule.imports`: ```ts import { StoreModule } from '@ngrx/store'; @@ -27,26 +28,53 @@ } ``` -3. Add `RouterState` to main application state: +2. Add `RouterState` to main application state: ```ts import { RouterState } from '@ngrx/router-store'; - + export interface AppState { router: RouterState; }; ``` -### Dispatching actions +### Dispatching Actions + + ```ts + import { go, replace, search, show, back, forward } from '@ngrx/router-store'; + ``` + +#### Navigation with a new state into history + + ```ts + store.dispatch(go(['/path', { routeParam: 1 }], { query: 'string' })); + ``` + +#### Navigation with replacing the current state in history + + ```ts + store.dispatch(replace(['/path'], { query: 'string' })); + ``` +#### Navigation without pushing a new state into history + + ```ts + store.dispatch(show(['/path'], { query: 'string' })); + ``` + +#### Navigation with only changing query parameters + + ```ts + store.dispatch(search({ query: 'string' })); + ``` + +#### Navigating back -```ts -import { go, replace, search, show, back, forward } from '@ngrx/router-store'; + ```ts + store.dispatch(back()); + ``` -// ... -store.dispatch(go('/path', { query: 'string' })); -store.dispatch(replace('/path', { query: 'string' })); -store.dispatch(search({ query: 'string' })); -store.dispatch(show('/path', { query: 'string' })); -store.dispatch(back()); -store.dispatch(forward()); -``` +#### Navigating forward + + ```ts + store.dispatch(forward()); + ``` diff --git a/spec/actions.spec.ts b/spec/actions.spec.ts index c7f3001..4c335e3 100644 --- a/spec/actions.spec.ts +++ b/spec/actions.spec.ts @@ -2,40 +2,64 @@ import * as actions from '../src/actions'; describe('Actions', function() { it('should provide a "go" action creator', function() { - expect(actions.go('/path', 'query=string')).toEqual({ + expect(actions.go('/path', { query: 'string' })).toEqual({ type: actions.routerActions.GO, payload: { path: '/path', - query: 'query=string' + query: { query: 'string' } + } + }); + + expect(actions.go(['/path'], { query: 'string' })).toEqual({ + type: actions.routerActions.GO, + payload: { + path: ['/path'], + query: { query: 'string' } } }); }); it('should provide a "replace" action creator', function() { - expect(actions.replace('/path', 'query=string')).toEqual({ + expect(actions.replace('/path', { query: 'string' })).toEqual({ type: actions.routerActions.REPLACE, payload: { path: '/path', - query: 'query=string' + query: { query: 'string' } + } + }); + + expect(actions.replace(['/path'], { query: 'string' })).toEqual({ + type: actions.routerActions.REPLACE, + payload: { + path: ['/path'], + query: { query: 'string' } } }); }); it('should provide a "show" action creator', function() { - expect(actions.show('/path', 'query=string')).toEqual({ + expect(actions.show('/path', { query: 'string' })).toEqual({ type: actions.routerActions.SHOW, payload: { path: '/path', - query: 'query=string' + query: { query: 'string' } + } + }); + + expect(actions.show(['/path'], { query: 'string' })).toEqual({ + type: actions.routerActions.SHOW, + payload: { + path: ['/path'], + query: { query: 'string' } } }); }); it('should provide a "search" action creator', function() { - expect(actions.search('query=string')).toEqual({ + expect(actions.search({ query: 'string' })).toEqual({ type: actions.routerActions.SEARCH, payload: { - query: 'query=string' + query: { query: 'string' } } }); }); diff --git a/spec/connect.spec.ts b/spec/connect.spec.ts index 44d172f..cb8d787 100644 --- a/spec/connect.spec.ts +++ b/spec/connect.spec.ts @@ -12,6 +12,9 @@ describe('Router/Store Connectors', function() { describe('listenForRouterMethodActions', function() { let router: any; let location: any; + let stringPath: string = '/path'; + let stringArray: any[] = [stringPath]; + let arrayPath: any[] = [stringPath, 1, { page: 1 }]; beforeEach(function() { location = { @@ -21,39 +24,68 @@ describe('Router/Store Connectors', function() { router = { navigate: jasmine.createSpy('navigate'), + navigateByUrl: jasmine.createSpy('navigate'), + parseUrl: jasmine.createSpy('navigate').and.returnValue({}), replace: jasmine.createSpy('replace'), search: jasmine.createSpy('search'), - show: jasmine.createSpy('show') + show: jasmine.createSpy('show'), + url: '' }; }); - it('should call Router@navigate when a "GO" action is dispatched', function() { - const action$ = Observable.of(routerActions.go('/path', 'query=string')); - listenForRouterMethodActions(router, location, action$); + describe('should call Router@navigate when a "GO" action is dispatched', function() { + it('with a string', function() { + const action$ = Observable.of(routerActions.go(stringPath, { query: 'string' })); + listenForRouterMethodActions(router, location, action$); + + expect(router.navigate).toHaveBeenCalledWith(stringArray, { queryParams: { query: 'string' } }); + }); + + it('with an array', function() { + const action$ = Observable.of(routerActions.go(arrayPath, { query: 'string' })); + listenForRouterMethodActions(router, location, action$); - expect(router.navigate).toHaveBeenCalledWith(['/path'], { queryParams: 'query=string' }); + expect(router.navigate).toHaveBeenCalledWith(['/path', 1, { page: 1 }], { queryParams: { query: 'string' } }); + }); }); - it('should call Router@navigate when a "REPLACE" action is dispatched', function() { - const action$ = Observable.of(routerActions.replace('/path', 'query=string')); - listenForRouterMethodActions(router, location, action$); + describe('should call Router@navigateByUrl when a "REPLACE" action is dispatched', function() { + it('with a string', function() { + const action$ = Observable.of(routerActions.replace(stringPath, { query: 'string' })); + listenForRouterMethodActions(router, location, action$); + + expect(router.navigate).toHaveBeenCalledWith(stringArray, { queryParams: { query: 'string' }, replaceUrl: true }); + }); + + it('with an array', function() { + const action$ = Observable.of(routerActions.replace(arrayPath, { query: 'string' })); + listenForRouterMethodActions(router, location, action$); - expect(router.navigate).toHaveBeenCalledWith(['/path'], { queryParams: 'query=string', replaceUrl: true }); + expect(router.navigate).toHaveBeenCalledWith(arrayPath, { queryParams: { query: 'string' }, replaceUrl: true }); + }); }); + describe('should call Router@navigate when a "SHOW" action is dispatched', function() { + it('with a string', function() { + const action$ = Observable.of(routerActions.show(stringPath, { query: 'string' })); + listenForRouterMethodActions(router, location, action$); - it('should call Router@navigate when a "SEARCH" action is dispatched', function() { - const action$ = Observable.of(routerActions.search('query=string')); - router.url = '/path'; - listenForRouterMethodActions(router, location, action$); + expect(router.navigate).toHaveBeenCalledWith(stringArray, { queryParams: { query: 'string' }, skipLocationChange: true }); + }); - expect(router.navigate).toHaveBeenCalledWith(['/path'], { queryParams: 'query=string' }); + it('with an array', function() { + const action$ = Observable.of(routerActions.show(arrayPath, { query: 'string' })); + listenForRouterMethodActions(router, location, action$); + + expect(router.navigate).toHaveBeenCalledWith(arrayPath, { queryParams: { query: 'string' }, skipLocationChange: true }); + }); }); - it('should call Router@navigate when a "SHOW" action is dispatched', function() { - const action$ = Observable.of(routerActions.show('/path', 'query=string')); + it('should call Router@navigate when a "SEARCH" action is dispatched', function() { + const action$ = Observable.of(routerActions.search({ query: 'string' })); + router.url = '/path'; listenForRouterMethodActions(router, location, action$); - expect(router.navigate).toHaveBeenCalledWith(['/path'], { queryParams: 'query=string', skipLocationChange: true }); + expect(router.navigateByUrl).toHaveBeenCalledWith({ queryParams: { query: 'string' } }); }); it('should call Location@back when a "BACK" action is dispatched', function() { diff --git a/src/actions.ts b/src/actions.ts index e74bc32..bb464ad 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; export interface RouterMethodCall { - path?: string; + path?: string | any[]; query?: any; } @@ -17,13 +17,13 @@ export const routerActions = { export const routerActionTypes = Object.keys(routerActions).map(key => routerActions[key]); -export function go(path: string, query?: any): Action { +export function go(path: string|any[], query?: any): Action { const payload: RouterMethodCall = { path, query }; return { type: routerActions.GO, payload }; } -export function replace(path: string, query?: any): Action { +export function replace(path: string|any[], query?: any): Action { const payload: RouterMethodCall = { path, query }; return { type: routerActions.REPLACE, payload }; @@ -35,7 +35,7 @@ export function search(query: any): Action { return { type: routerActions.SEARCH, payload }; } -export function show(path: string, query?: any): Action { +export function show(path: string|any[], query?: any): Action { const payload: RouterMethodCall = { path, query }; return { type: routerActions.SHOW, payload }; diff --git a/src/connect.ts b/src/connect.ts index 2f34768..ead70ac 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -4,7 +4,7 @@ import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/withLatestFrom'; import '@ngrx/core/add/operator/select'; -import { Router, Event, NavigationEnd } from '@angular/router'; +import { Router, Event, NavigationEnd, UrlTree } from '@angular/router'; import { Location } from '@angular/common'; import { Store, Action } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; @@ -17,7 +17,7 @@ export function listenForRouterMethodActions(router: Router, location: Location, .filter(action => routerActionTypes.indexOf(action.type) > -1) .subscribe(action => { const { path, query: queryParams }: RouterMethodCall = action.payload; - let commands: any[] = [path]; + let commands: any[] = Array.isArray(path) ? path : [path]; switch (action.type) { case routerActions.GO: @@ -29,9 +29,9 @@ export function listenForRouterMethodActions(router: Router, location: Location, break; case routerActions.SEARCH: - let url = router.url; - let command = url.split(/\?/)[0]; - router.navigate([command], { queryParams }); + let urlTree: UrlTree = router.parseUrl(router.url); + urlTree.queryParams = queryParams; + router.navigateByUrl(urlTree); break; case routerActions.SHOW: diff --git a/tests.ts b/tests.ts index 26523ea..4ca18c2 100644 --- a/tests.ts +++ b/tests.ts @@ -19,16 +19,16 @@ Promise.all([ System.import('@angular/core/testing'), System.import('@angular/platform-browser-dynamic/testing') ]) - // First, initialize the Angular testing environment. - .then(([testing, testingBrowser]) => { - testing.getTestBed().initTestEnvironment( - testingBrowser.BrowserDynamicTestingModule, - testingBrowser.platformBrowserDynamicTesting() - ); - }) - // Then we find all the tests. - .then(() => (require).context('./spec', true, /\.spec\.ts/)) - // And load the modules. - .then(context => context.keys().map(context)) - // Finally, start Karma to run the tests. - .then(__karma__.start, __karma__.error); +// First, initialize the Angular testing environment. +.then(([testing, testingBrowser]) => { + testing.getTestBed().initTestEnvironment( + testingBrowser.BrowserDynamicTestingModule, + testingBrowser.platformBrowserDynamicTesting() + ); +}) +// Then we find all the tests. +.then(() => (require).context('./spec', true, /\.spec\.ts/)) +// And load the modules. +.then(context => context.keys().map(context)) +// Finally, start Karma to run the tests. +.then(__karma__.start, __karma__.error);