From 998b9d2de126423a2f83f7ab74d71f858e78991c Mon Sep 17 00:00:00 2001 From: Peter B Smith Date: Sat, 21 Oct 2017 15:27:03 -0400 Subject: [PATCH 1/5] chore(Example): Improve handling of book search error (#483) Closes #466 --- example-app/app/books/actions/book.ts | 9 +- .../app/books/components/book-search.ts | 10 +- .../app/books/containers/find-book-page.ts | 4 +- example-app/app/books/effects/book.spec.ts | 8 +- example-app/app/books/effects/book.ts | 2 +- example-app/app/books/reducers/index.ts | 4 + example-app/app/books/reducers/search.ts | 17 +- modules/router-store/spec/integration.spec.ts | 219 +++++++++--------- 8 files changed, 157 insertions(+), 116 deletions(-) diff --git a/example-app/app/books/actions/book.ts b/example-app/app/books/actions/book.ts index e1735e87fb..292c430ddf 100644 --- a/example-app/app/books/actions/book.ts +++ b/example-app/app/books/actions/book.ts @@ -3,6 +3,7 @@ import { Book } from '../models/book'; export const SEARCH = '[Book] Search'; export const SEARCH_COMPLETE = '[Book] Search Complete'; +export const SEARCH_ERROR = '[Book] Search Error'; export const LOAD = '[Book] Load'; export const SELECT = '[Book] Select'; @@ -25,6 +26,12 @@ export class SearchComplete implements Action { constructor(public payload: Book[]) {} } +export class SearchError implements Action { + readonly type = SEARCH_ERROR; + + constructor(public payload: string) {} +} + export class Load implements Action { readonly type = LOAD; @@ -41,4 +48,4 @@ export class Select implements Action { * Export a type alias of all actions in this action group * so that reducers can easily compose action types */ -export type Actions = Search | SearchComplete | Load | Select; +export type Actions = Search | SearchComplete | SearchError | Load | Select; diff --git a/example-app/app/books/components/book-search.ts b/example-app/app/books/components/book-search.ts index 151db7e669..46d3816c0d 100644 --- a/example-app/app/books/components/book-search.ts +++ b/example-app/app/books/components/book-search.ts @@ -14,16 +14,23 @@ import { Component, Output, Input, EventEmitter } from '@angular/core'; + {{error}} `, styles: [ ` md-card-title, - md-card-content { + md-card-content, + md-card-footer { display: flex; justify-content: center; } + md-card-footer { + color: #FF0000; + padding: 5px 0; + } + input { width: 300px; } @@ -50,5 +57,6 @@ import { Component, Output, Input, EventEmitter } from '@angular/core'; export class BookSearchComponent { @Input() query = ''; @Input() searching = false; + @Input() error = ''; @Output() search = new EventEmitter(); } diff --git a/example-app/app/books/containers/find-book-page.ts b/example-app/app/books/containers/find-book-page.ts index 86fda79483..433a334fd3 100644 --- a/example-app/app/books/containers/find-book-page.ts +++ b/example-app/app/books/containers/find-book-page.ts @@ -11,7 +11,7 @@ import { Book } from '../models/book'; selector: 'bc-find-book-page', changeDetection: ChangeDetectionStrategy.OnPush, template: ` - + `, }) @@ -19,11 +19,13 @@ export class FindBookPageComponent { searchQuery$: Observable; books$: Observable; loading$: Observable; + error$: Observable; constructor(private store: Store) { this.searchQuery$ = store.select(fromBooks.getSearchQuery).take(1); this.books$ = store.select(fromBooks.getSearchResults); this.loading$ = store.select(fromBooks.getSearchLoading); + this.error$ = store.select(fromBooks.getSearchError); } search(query: string) { diff --git a/example-app/app/books/effects/book.spec.ts b/example-app/app/books/effects/book.spec.ts index c2b7e07c40..6724fa09fe 100644 --- a/example-app/app/books/effects/book.spec.ts +++ b/example-app/app/books/effects/book.spec.ts @@ -5,7 +5,7 @@ import { empty } from 'rxjs/observable/empty'; import { BookEffects, SEARCH_SCHEDULER, SEARCH_DEBOUNCE } from './book'; import { GoogleBooksService } from '../../core/services/google-books'; import { Observable } from 'rxjs/Observable'; -import { Search, SearchComplete } from '../actions/book'; +import { Search, SearchComplete, SearchError } from '../actions/book'; import { Book } from '../models/book'; export class TestActions extends Actions { @@ -62,10 +62,10 @@ describe('BookEffects', () => { expect(effects.search$).toBeObservable(expected); }); - it('should return a new book.SearchComplete, with an empty array, if the books service throws', () => { + it('should return a new book.SearchError if the books service throws', () => { const action = new Search('query'); - const completion = new SearchComplete([]); - const error = 'Error!'; + const completion = new SearchError('Unexpected Error. Try again later.'); + const error = 'Unexpected Error. Try again later.'; actions$.stream = hot('-a---', { a: action }); const response = cold('-#|', {}, error); diff --git a/example-app/app/books/effects/book.ts b/example-app/app/books/effects/book.ts index 75804c8347..82a1cb30b6 100644 --- a/example-app/app/books/effects/book.ts +++ b/example-app/app/books/effects/book.ts @@ -51,7 +51,7 @@ export class BookEffects { .searchBooks(query) .takeUntil(nextSearch$) .map((books: Book[]) => new book.SearchComplete(books)) - .catch(() => of(new book.SearchComplete([]))); + .catch(err => of(new book.SearchError(err))); }); constructor( diff --git a/example-app/app/books/reducers/index.ts b/example-app/app/books/reducers/index.ts index 3ca7970199..d56a4bf5ab 100644 --- a/example-app/app/books/reducers/index.ts +++ b/example-app/app/books/reducers/index.ts @@ -105,6 +105,10 @@ export const getSearchLoading = createSelector( getSearchState, fromSearch.getLoading ); +export const getSearchError = createSelector( + getSearchState, + fromSearch.getError +); /** * Some selector functions create joins across parts of state. This selector diff --git a/example-app/app/books/reducers/search.ts b/example-app/app/books/reducers/search.ts index 381285fd27..f6c38ff18f 100644 --- a/example-app/app/books/reducers/search.ts +++ b/example-app/app/books/reducers/search.ts @@ -3,12 +3,14 @@ import * as book from '../actions/book'; export interface State { ids: string[]; loading: boolean; + error: string; query: string; } const initialState: State = { ids: [], loading: false, + error: '', query: '', }; @@ -21,14 +23,16 @@ export function reducer(state = initialState, action: book.Actions): State { return { ids: [], loading: false, + error: '', query, }; } return { ...state, - query, loading: true, + error: '', + query, }; } @@ -36,10 +40,19 @@ export function reducer(state = initialState, action: book.Actions): State { return { ids: action.payload.map(book => book.id), loading: false, + error: '', query: state.query, }; } + case book.SEARCH_ERROR: { + return { + ...state, + loading: false, + error: action.payload, + }; + } + default: { return state; } @@ -51,3 +64,5 @@ export const getIds = (state: State) => state.ids; export const getQuery = (state: State) => state.query; export const getLoading = (state: State) => state.loading; + +export const getError = (state: State) => state.error; diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index dee373c1db..0aa23ebc9c 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -29,7 +29,7 @@ describe('integration spec', () => { } }; - createTestModule({reducers: {reducer}}); + createTestModule({ reducers: { reducer } }); const router: Router = TestBed.get(Router); const store = TestBed.get(Store); @@ -39,18 +39,18 @@ describe('integration spec', () => { .navigateByUrl('/') .then(() => { expect(log).toEqual([ - {type: 'store', state: ''}, // init event. has nothing to do with the router - {type: 'router', event: 'NavigationStart', url: '/'}, - {type: 'router', event: 'RoutesRecognized', url: '/'}, - {type: 'store', state: '/'}, // ROUTER_NAVIGATION event in the store + { type: 'store', state: '' }, // init event. has nothing to do with the router + { type: 'router', event: 'NavigationStart', url: '/' }, + { type: 'router', event: 'RoutesRecognized', url: '/' }, + { type: 'store', state: '/' }, // ROUTER_NAVIGATION event in the store /* new Router Lifecycle in Angular 4.3 */ - {type: 'router', event: 'GuardsCheckStart', url: '/'}, - {type: 'router', event: 'GuardsCheckEnd', url: '/'}, - {type: 'router', event: 'ResolveStart', url: '/'}, - {type: 'router', event: 'ResolveEnd', url: '/'}, + { type: 'router', event: 'GuardsCheckStart', url: '/' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/' }, + { type: 'router', event: 'ResolveStart', url: '/' }, + { type: 'router', event: 'ResolveEnd', url: '/' }, - {type: 'router', event: 'NavigationEnd', url: '/'}, + { type: 'router', event: 'NavigationEnd', url: '/' }, ]); }) .then(() => { @@ -59,17 +59,17 @@ describe('integration spec', () => { }) .then(() => { expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, - {type: 'store', state: '/next'}, + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, + { type: 'store', state: '/next' }, /* 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: 'router', event: 'ResolveEnd', url: '/next'}, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, + { type: 'router', event: 'ResolveStart', url: '/next' }, + { type: 'router', event: 'ResolveEnd', url: '/next' }, - {type: 'router', event: 'NavigationEnd', url: '/next'}, + { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); done(); @@ -88,7 +88,7 @@ describe('integration spec', () => { } }; - createTestModule({reducers: {reducer}}); + createTestModule({ reducers: { reducer } }); const router: Router = TestBed.get(Router); const store = TestBed.get(Store); @@ -103,9 +103,9 @@ describe('integration spec', () => { .catch(e => { expect(e.message).toEqual('You shall not pass!'); expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, - {type: 'router', event: 'NavigationError', url: '/next'}, + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, + { type: 'router', event: 'NavigationError', url: '/next' }, ]); done(); @@ -131,7 +131,7 @@ describe('integration spec', () => { }; createTestModule({ - reducers: {reducer, routerReducer}, + reducers: { reducer, routerReducer }, canActivate: () => false, }); @@ -149,29 +149,28 @@ describe('integration spec', () => { expect(r).toEqual(false); expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, { type: 'store', - state: {url: '/next', lastAction: ROUTER_NAVIGATION}, + state: { url: '/next', lastAction: ROUTER_NAVIGATION }, }, /* new Router Lifecycle in Angular 4.3 - m */ - {type: 'router', event: 'GuardsCheckStart', url: '/next'}, - {type: 'router', event: 'GuardsCheckEnd', url: '/next'}, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, // { type: 'router', event: 'ResolveStart', url: '/next' }, // { type: 'router', event: 'ResolveEnd', url: '/next' }, - { type: 'store', state: { url: '/next', lastAction: ROUTER_CANCEL, - storeState: {url: '/next', lastAction: ROUTER_NAVIGATION}, + storeState: { url: '/next', lastAction: ROUTER_NAVIGATION }, }, }, - {type: 'router', event: 'NavigationCancel', url: '/next'}, + { type: 'router', event: 'NavigationCancel', url: '/next' }, ]); done(); @@ -197,7 +196,7 @@ describe('integration spec', () => { }; createTestModule({ - reducers: {reducer, routerReducer}, + reducers: { reducer, routerReducer }, canActivate: () => { throw new Error('BOOM!'); }, @@ -217,40 +216,42 @@ describe('integration spec', () => { expect(e.message).toEqual('BOOM!'); expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, { type: 'store', - state: {url: '/next', lastAction: ROUTER_NAVIGATION}, + state: { url: '/next', lastAction: ROUTER_NAVIGATION }, }, /* new Router Lifecycle in Angular 4.3 */ - {type: 'router', event: 'GuardsCheckStart', url: '/next'}, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, { type: 'store', state: { url: '/next', lastAction: ROUTER_ERROR, - storeState: {url: '/next', lastAction: ROUTER_NAVIGATION}, + storeState: { url: '/next', lastAction: ROUTER_NAVIGATION }, }, }, - {type: 'router', event: 'NavigationError', url: '/next'}, + { type: 'router', event: 'NavigationError', url: '/next' }, ]); done(); }); }); - it('should call navigateByUrl when resetting state of the routerReducer', (done: any) => { + it('should call navigateByUrl when resetting state of the routerReducer', ( + done: any + ) => { const reducer = (state: any, action: RouterAction) => { const r = routerReducer(state, action); return r && r.state - ? {url: r.state.url, navigationId: r.navigationId} + ? { url: r.state.url, navigationId: r.navigationId } : null; }; - createTestModule({reducers: {routerReducer, reducer}}); + createTestModule({ reducers: { routerReducer, reducer } }); const router = TestBed.get(Router); const store = TestBed.get(Store); @@ -271,17 +272,17 @@ describe('integration spec', () => { }) .then(() => { expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, - {type: 'store', state: {url: '/next', navigationId: 2}}, + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, + { type: 'store', state: { url: '/next', navigationId: 2 } }, /* 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: 'router', event: 'ResolveEnd', url: '/next'}, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, + { type: 'router', event: 'ResolveStart', url: '/next' }, + { type: 'router', event: 'ResolveEnd', url: '/next' }, - {type: 'router', event: 'NavigationEnd', url: '/next'}, + { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); log.splice(0); @@ -289,24 +290,24 @@ describe('integration spec', () => { type: ROUTER_NAVIGATION, payload: { routerState: routerReducerStates[0].state, - event: {id: routerReducerStates[0].navigationId}, + event: { id: routerReducerStates[0].navigationId }, }, }); return waitForNavigation(router); }) .then(() => { expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/'}, - {type: 'store', state: {url: '/', navigationId: 1}}, // restored - {type: 'router', event: 'RoutesRecognized', url: '/'}, + { type: 'router', event: 'NavigationStart', url: '/' }, + { type: 'store', state: { url: '/', navigationId: 1 } }, // restored + { type: 'router', event: 'RoutesRecognized', url: '/' }, /* new Router Lifecycle in Angular 4.3 */ - {type: 'router', event: 'GuardsCheckStart', url: '/'}, - {type: 'router', event: 'GuardsCheckEnd', url: '/'}, - {type: 'router', event: 'ResolveStart', url: '/'}, - {type: 'router', event: 'ResolveEnd', url: '/'}, + { type: 'router', event: 'GuardsCheckStart', url: '/' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/' }, + { type: 'router', event: 'ResolveStart', url: '/' }, + { type: 'router', event: 'ResolveEnd', url: '/' }, - {type: 'router', event: 'NavigationEnd', url: '/'}, + { type: 'router', event: 'NavigationEnd', url: '/' }, ]); log.splice(0); }) @@ -315,39 +316,41 @@ describe('integration spec', () => { type: ROUTER_NAVIGATION, payload: { routerState: routerReducerStates[1].state, - event: {id: routerReducerStates[1].navigationId}, + event: { id: routerReducerStates[1].navigationId }, }, }); return waitForNavigation(router); }) .then(() => { expect(log).toEqual([ - {type: 'store', state: {url: '/next', navigationId: 2}}, // restored - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, + { type: 'store', state: { url: '/next', navigationId: 2 } }, // restored + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, /* 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: 'router', event: 'ResolveEnd', url: '/next'}, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, + { type: 'router', event: 'ResolveStart', url: '/next' }, + { type: 'router', event: 'ResolveEnd', url: '/next' }, - {type: 'router', event: 'NavigationEnd', url: '/next'}, + { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); done(); }); }); - it('should support cancellation of initial navigation using canLoad guard', (done: any) => { + it('should support cancellation of initial navigation using canLoad guard', ( + done: any + ) => { const reducer = (state: any, action: RouterAction) => { const r = routerReducer(state, action); return r && r.state - ? {url: r.state.url, navigationId: r.navigationId} + ? { url: r.state.url, navigationId: r.navigationId } : null; }; createTestModule({ - reducers: {routerReducer, reducer}, + reducers: { routerReducer, reducer }, canLoad: () => false, }); @@ -359,19 +362,21 @@ describe('integration spec', () => { expect(r).toBe(false); expect(log).toEqual([ - {type: 'store', state: null}, - {type: 'router', event: 'NavigationStart', url: '/load'}, - {type: 'store', state: null}, - {type: 'router', event: 'NavigationCancel', url: '/load'}, + { type: 'store', state: null }, + { type: 'router', event: 'NavigationStart', url: '/load' }, + { type: 'store', state: null }, + { type: 'router', event: 'NavigationCancel', url: '/load' }, ]); done(); }); - it('should support a custom RouterStateSnapshot serializer ', (done: any) => { + it('should support a custom RouterStateSnapshot serializer ', ( + done: any + ) => { const reducer = (state: any, action: RouterAction) => { const r = routerReducer(state, action); return r && r.state - ? {url: r.state.url, navigationId: r.navigationId} + ? { url: r.state.url, navigationId: r.navigationId } : null; }; @@ -379,17 +384,17 @@ describe('integration spec', () => { implements RouterStateSerializer<{ url: string; params: any }> { serialize(routerState: RouterStateSnapshot) { const url = `${routerState.url}-custom`; - const params = {test: 1}; + const params = { test: 1 }; - return {url, params}; + return { url, params }; } } const providers = [ - {provide: RouterStateSerializer, useClass: CustomSerializer}, + { provide: RouterStateSerializer, useClass: CustomSerializer }, ]; - createTestModule({reducers: {routerReducer, reducer}, providers}); + createTestModule({ reducers: { routerReducer, reducer }, providers }); const router = TestBed.get(Router); const store = TestBed.get(Store); @@ -403,17 +408,17 @@ describe('integration spec', () => { }) .then(() => { expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, { type: 'store', state: { url: '/next-custom', navigationId: 2, - params: {test: 1}, + params: { test: 1 }, }, }, - {type: 'router', event: 'NavigationEnd', url: '/next'}, + { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); log.splice(0); done(); @@ -423,9 +428,9 @@ describe('integration spec', () => { it('should support event during an async canActivate guard', (done: any) => { createTestModule({ - reducers: {routerReducer}, + reducers: { routerReducer }, canActivate: () => { - store.dispatch({type: 'USER_EVENT'}); + store.dispatch({ type: 'USER_EVENT' }); return store.take(1).mapTo(true); }, }); @@ -442,18 +447,18 @@ describe('integration spec', () => { }) .then(() => { expect(log).toEqual([ - {type: 'router', event: 'NavigationStart', url: '/next'}, - {type: 'router', event: 'RoutesRecognized', url: '/next'}, - {type: 'store', state: undefined}, // after ROUTER_NAVIGATION + { type: 'router', event: 'NavigationStart', url: '/next' }, + { type: 'router', event: 'RoutesRecognized', url: '/next' }, + { type: 'store', state: undefined }, // after ROUTER_NAVIGATION /* new Router Lifecycle in Angular 4.3 */ - {type: 'router', event: 'GuardsCheckStart', url: '/next'}, - {type: 'store', state: undefined}, // after USER_EVENT - {type: 'router', event: 'GuardsCheckEnd', url: '/next'}, - {type: 'router', event: 'ResolveStart', url: '/next'}, - {type: 'router', event: 'ResolveEnd', url: '/next'}, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, + { type: 'store', state: undefined }, // after USER_EVENT + { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, + { type: 'router', event: 'ResolveStart', url: '/next' }, + { type: 'router', event: 'ResolveEnd', url: '/next' }, - {type: 'router', event: 'NavigationEnd', url: '/next'}, + { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); done(); @@ -461,32 +466,32 @@ describe('integration spec', () => { }); }); -function createTestModule(opts: { - reducers?: any; - canActivate?: Function; - canLoad?: Function; - providers?: Provider[]; -} = {}) { +function createTestModule( + opts: { + reducers?: any; + canActivate?: Function; + canLoad?: Function; + providers?: Provider[]; + } = {} +) { @Component({ selector: 'test-app', template: '', }) - class AppCmp { - } + class AppCmp {} @Component({ selector: 'pagea-cmp', template: 'pagea-cmp', }) - class SimpleCmp { - } + class SimpleCmp {} TestBed.configureTestingModule({ declarations: [AppCmp, SimpleCmp], imports: [ StoreModule.forRoot(opts.reducers), RouterTestingModule.withRoutes([ - {path: '', component: SimpleCmp}, + { path: '', component: SimpleCmp }, { path: 'next', component: SimpleCmp, @@ -532,6 +537,6 @@ function logOfRouterAndStore(router: Router, store: Store): any[] { url: (e).url.toString(), }) ); - store.subscribe(store => log.push({type: 'store', state: store.reducer})); + store.subscribe(store => log.push({ type: 'store', state: store.reducer })); return log; } From 7927f8ec950b5e5faf4cce95f0b0f49d7185d0ec Mon Sep 17 00:00:00 2001 From: Abdellatif Ait boudad Date: Sun, 22 Oct 2017 20:34:34 +0100 Subject: [PATCH 2/5] docs(readme): Add @ngrx/entity (#521) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4ebdd5c748..4554ea7e95 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Reactive libraries for Angular - [@ngrx/router-store](./docs/router-store/README.md) - Bindings to connect the Angular Router to @ngrx/store - [@ngrx/store-devtools](./docs/store-devtools/README.md) - Store instrumentation that enables a [powerful time-travelling debugger](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en). +- [@ngrx/entity](./docs/entity/README.md) - Entity State adapter for managing record collections. ## Examples - [example-app](./example-app/README.md) - Example application utilizing @ngrx libraries, showcasing common patterns and best practices. From f56fb54b19d10bf3fcdea028d213131ca3b13b1d Mon Sep 17 00:00:00 2001 From: Ryan Jordan Date: Sun, 22 Oct 2017 22:51:50 +0300 Subject: [PATCH 3/5] chore(Example): Replace Http with HttpClient (#520) --- example-app/app/app.module.ts | 4 ++-- .../app/core/services/google-books.spec.ts | 20 ++++++------------- example-app/app/core/services/google-books.ts | 10 +++++----- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/example-app/app/app.module.ts b/example-app/app/app.module.ts index 4eac09b73b..8df2b53641 100644 --- a/example-app/app/app.module.ts +++ b/example-app/app/app.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; -import { HttpModule } from '@angular/http'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; @@ -30,7 +30,7 @@ import { environment } from '../environments/environment'; CommonModule, BrowserModule, BrowserAnimationsModule, - HttpModule, + HttpClientModule, RouterModule.forRoot(routes, { useHash: true }), /** diff --git a/example-app/app/core/services/google-books.spec.ts b/example-app/app/core/services/google-books.spec.ts index d525ef1e0a..fd1692987c 100644 --- a/example-app/app/core/services/google-books.spec.ts +++ b/example-app/app/core/services/google-books.spec.ts @@ -1,22 +1,22 @@ import { TestBed } from '@angular/core/testing'; -import { Http } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; import { cold } from 'jasmine-marbles'; import { GoogleBooksService } from './google-books'; describe('Service: GoogleBooks', () => { let service: GoogleBooksService; - let http: any; + let http: HttpClient; beforeEach(() => { TestBed.configureTestingModule({ providers: [ - { provide: Http, useValue: { get: jest.fn() } }, + { provide: HttpClient, useValue: { get: jest.fn() } }, GoogleBooksService, ], }); service = TestBed.get(GoogleBooksService); - http = TestBed.get(Http); + http = TestBed.get(HttpClient); }); const data = { @@ -35,11 +35,7 @@ describe('Service: GoogleBooks', () => { const queryTitle = 'Book Title'; it('should call the search api and return the search results', () => { - const httpResponse = { - json: () => books, - }; - - const response = cold('-a|', { a: httpResponse }); + const response = cold('-a|', { a: books }); const expected = cold('-b|', { b: books.items }); http.get = jest.fn(() => response); @@ -50,11 +46,7 @@ describe('Service: GoogleBooks', () => { }); it('should retrieve the book from the volumeId', () => { - const httpResponse = { - json: () => data, - }; - - const response = cold('-a|', { a: httpResponse }); + const response = cold('-a|', { a: data }); const expected = cold('-b|', { b: data }); http.get = jest.fn(() => response); diff --git a/example-app/app/core/services/google-books.ts b/example-app/app/core/services/google-books.ts index 7277951eaf..ce1b1d4aee 100644 --- a/example-app/app/core/services/google-books.ts +++ b/example-app/app/core/services/google-books.ts @@ -1,6 +1,6 @@ import 'rxjs/add/operator/map'; import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { Book } from '../../books/models/book'; @@ -8,15 +8,15 @@ import { Book } from '../../books/models/book'; export class GoogleBooksService { private API_PATH = 'https://www.googleapis.com/books/v1/volumes'; - constructor(private http: Http) {} + constructor(private http: HttpClient) {} searchBooks(queryTitle: string): Observable { return this.http - .get(`${this.API_PATH}?q=${queryTitle}`) - .map(res => res.json().items || []); + .get<{ items: Book[] }>(`${this.API_PATH}?q=${queryTitle}`) + .map(books => books.items || []); } retrieveBook(volumeId: string): Observable { - return this.http.get(`${this.API_PATH}/${volumeId}`).map(res => res.json()); + return this.http.get(`${this.API_PATH}/${volumeId}`); } } From 4db7d61340eb66eed76d3d1c951a8fc755f99f93 Mon Sep 17 00:00:00 2001 From: Ryan Jordan Date: Mon, 23 Oct 2017 01:26:28 +0300 Subject: [PATCH 4/5] chore(deps): Add Angular 5 RC support (#522) --- modules/effects/package.json | 6 ++---- modules/entity/package.json | 6 ++---- modules/router-store/package.json | 16 +++++----------- modules/store-devtools/package.json | 8 +------- modules/store/package.json | 8 ++------ 5 files changed, 12 insertions(+), 32 deletions(-) diff --git a/modules/effects/package.json b/modules/effects/package.json index 669866958f..330eabafd3 100644 --- a/modules/effects/package.json +++ b/modules/effects/package.json @@ -10,12 +10,10 @@ "type": "git", "url": "https://github.com/ngrx/platform.git" }, - "authors": [ - "Mike Ryan" - ], + "authors": ["Mike Ryan"], "license": "MIT", "peerDependencies": { - "@angular/core": "^4.0.0", + "@angular/core": "^4.0.0 || ^5.0.0-rc.2", "@ngrx/store": "^4.0.0", "rxjs": "^5.0.0" } diff --git a/modules/entity/package.json b/modules/entity/package.json index 44484ef442..c74576ab37 100644 --- a/modules/entity/package.json +++ b/modules/entity/package.json @@ -10,12 +10,10 @@ "type": "git", "url": "https://github.com/ngrx/platform.git" }, - "authors": [ - "Mike Ryan" - ], + "authors": ["Mike Ryan"], "license": "MIT", "peerDependencies": { - "@angular/core": "^4.0.0", + "@angular/core": "^4.0.0 || ^5.0.0-rc.2", "@ngrx/store": "^4.0.0", "rxjs": "^5.0.0" } diff --git a/modules/router-store/package.json b/modules/router-store/package.json index 455a69178f..9573fc78cb 100644 --- a/modules/router-store/package.json +++ b/modules/router-store/package.json @@ -10,19 +10,13 @@ "type": "git", "url": "git+https://github.com/ngrx/platform.git" }, - "keywords": [ - "RxJS", - "Angular", - "Redux" - ], - "authors": [ - "Victor Savkin " - ], + "keywords": ["RxJS", "Angular", "Redux"], + "authors": ["Victor Savkin "], "license": "MIT", "peerDependencies": { - "@angular/common": "^4.0.0", - "@angular/core": "^4.0.0", - "@angular/router": "^4.0.0", + "@angular/common": "^4.0.0 || ^5.0.0-rc.2", + "@angular/core": "^4.0.0 || ^5.0.0-rc.2", + "@angular/router": "^4.0.0 || ^5.0.0-rc.2", "@ngrx/store": "^4.0.0", "rxjs": "^5.0.0" } diff --git a/modules/store-devtools/package.json b/modules/store-devtools/package.json index 9ede42dbe8..0411068639 100644 --- a/modules/store-devtools/package.json +++ b/modules/store-devtools/package.json @@ -10,13 +10,7 @@ "type": "git", "url": "git+https://github.com/ngrx/platform.git" }, - "keywords": [ - "RxJS", - "Angular", - "Redux", - "Store", - "@ngrx/store" - ], + "keywords": ["RxJS", "Angular", "Redux", "Store", "@ngrx/store"], "contributors": [ { "name": "Rob Wormald", diff --git a/modules/store/package.json b/modules/store/package.json index e461fd6458..d2c17a6478 100644 --- a/modules/store/package.json +++ b/modules/store/package.json @@ -10,11 +10,7 @@ "type": "git", "url": "git+https://github.com/ngrx/platform.git" }, - "keywords": [ - "RxJS", - "Angular", - "Redux" - ], + "keywords": ["RxJS", "Angular", "Redux"], "author": "Rob Wormald ", "license": "MIT", "bugs": { @@ -22,7 +18,7 @@ }, "homepage": "https://github.com/ngrx/platform#readme", "peerDependencies": { - "@angular/core": "^4.0.0", + "@angular/core": "^4.0.0 || ^5.0.0-rc.2", "rxjs": "^5.0.0" } } From ab7de5c97909aca15fc2366d8daaed8c70a7b56d Mon Sep 17 00:00:00 2001 From: Phill Z Date: Mon, 23 Oct 2017 14:01:42 +1100 Subject: [PATCH 5/5] feat(RouterStore): Add configurable option for router reducer name (#417) Closes #410 --- modules/router-store/spec/integration.spec.ts | 56 ++++++++++++- modules/router-store/src/index.ts | 4 + .../router-store/src/router_store_module.ts | 81 +++++++++++++++++-- yarn.lock | 2 +- 4 files changed, 133 insertions(+), 10 deletions(-) diff --git a/modules/router-store/spec/integration.spec.ts b/modules/router-store/spec/integration.spec.ts index 0aa23ebc9c..9c753ad729 100644 --- a/modules/router-store/spec/integration.spec.ts +++ b/modules/router-store/spec/integration.spec.ts @@ -1,3 +1,4 @@ +import { StoreRouterConfig } from '../src/router_store_module'; import { Component, Provider } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router'; @@ -457,7 +458,59 @@ describe('integration spec', () => { { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, { type: 'router', event: 'ResolveStart', url: '/next' }, { type: 'router', event: 'ResolveEnd', url: '/next' }, + { type: 'router', event: 'NavigationEnd', url: '/next' }, + ]); + done(); + }); + }); + + it('should work when defining state key', (done: any) => { + const reducer = (state: string = '', action: RouterAction) => { + if (action.type === ROUTER_NAVIGATION) { + return action.payload.routerState.url.toString(); + } else { + return state; + } + }; + + createTestModule({ + reducers: { reducer }, + config: { stateKey: 'router-reducer' }, + }); + + const router: Router = TestBed.get(Router); + const store = TestBed.get(Store); + const log = logOfRouterAndStore(router, store); + + router + .navigateByUrl('/') + .then(() => { + expect(log).toEqual([ + { type: 'store', state: '' }, // init event. has nothing to do with the router + { type: 'router', event: 'NavigationStart', url: '/' }, + { type: 'router', event: 'RoutesRecognized', url: '/' }, + { type: 'store', state: '/' }, // ROUTER_NAVIGATION event in the store + { type: 'router', event: 'GuardsCheckStart', url: '/' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/' }, + { type: 'router', event: 'ResolveStart', url: '/' }, + { type: 'router', event: 'ResolveEnd', url: '/' }, + { type: 'router', event: 'NavigationEnd', url: '/' }, + ]); + }) + .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: '/next' }, + { type: 'router', event: 'GuardsCheckStart', url: '/next' }, + { type: 'router', event: 'GuardsCheckEnd', url: '/next' }, + { type: 'router', event: 'ResolveStart', url: '/next' }, + { type: 'router', event: 'ResolveEnd', url: '/next' }, { type: 'router', event: 'NavigationEnd', url: '/next' }, ]); @@ -472,6 +525,7 @@ function createTestModule( canActivate?: Function; canLoad?: Function; providers?: Provider[]; + config?: StoreRouterConfig; } = {} ) { @Component({ @@ -503,7 +557,7 @@ function createTestModule( canLoad: ['CanLoadNext'], }, ]), - StoreRouterConnectingModule, + StoreRouterConnectingModule.forRoot(opts.config), ], providers: [ { diff --git a/modules/router-store/src/index.ts b/modules/router-store/src/index.ts index ef9b177f2e..550f4d39b5 100644 --- a/modules/router-store/src/index.ts +++ b/modules/router-store/src/index.ts @@ -12,6 +12,10 @@ export { RouterCancelPayload, RouterNavigationPayload, StoreRouterConnectingModule, + StoreRouterConfig, + StoreRouterConfigFunction, + ROUTER_CONFIG, + DEFAULT_ROUTER_FEATURENAME, } from './router_store_module'; export { diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index b65a5990b3..1b805f88b6 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -1,4 +1,9 @@ -import { NgModule } from '@angular/core'; +import { + NgModule, + ModuleWithProviders, + InjectionToken, + Inject, +} from '@angular/core'; import { NavigationCancel, NavigationError, @@ -107,6 +112,33 @@ export function routerReducer( } } +export type StoreRouterConfig = { + stateKey?: string; +}; + +export const _ROUTER_CONFIG = new InjectionToken( + '@ngrx/router-store Internal Configuration' +); +export const ROUTER_CONFIG = new InjectionToken( + '@ngrx/router-store Configuration' +); +export const DEFAULT_ROUTER_FEATURENAME = 'routerReducer'; + +export function _createDefaultRouterConfig(config: any): StoreRouterConfig { + let _config = {}; + + if (typeof config === 'function') { + _config = config(); + } + + return { + stateKey: DEFAULT_ROUTER_FEATURENAME, + ..._config, + }; +} + +export type StoreRouterConfigFunction = () => StoreRouterConfig; + /** * Connects RouterModule with StoreModule. * @@ -152,9 +184,37 @@ export function routerReducer( @NgModule({ providers: [ { provide: RouterStateSerializer, useClass: DefaultRouterStateSerializer }, + { + provide: _ROUTER_CONFIG, + useValue: { stateKey: DEFAULT_ROUTER_FEATURENAME }, + }, + { + provide: ROUTER_CONFIG, + useFactory: _createDefaultRouterConfig, + deps: [_ROUTER_CONFIG], + }, ], }) export class StoreRouterConnectingModule { + static forRoot( + config?: StoreRouterConfig | StoreRouterConfigFunction + ): ModuleWithProviders; + static forRoot( + config: StoreRouterConfig | StoreRouterConfigFunction = {} + ): ModuleWithProviders { + return { + ngModule: StoreRouterConnectingModule, + providers: [ + { provide: _ROUTER_CONFIG, useValue: config }, + { + provide: ROUTER_CONFIG, + useFactory: _createDefaultRouterConfig, + deps: [_ROUTER_CONFIG], + }, + ], + }; + } + private routerState: RouterStateSnapshot; private storeState: any; private lastRoutesRecognized: RoutesRecognized; @@ -162,11 +222,16 @@ 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( private store: Store, private router: Router, - private serializer: RouterStateSerializer + private serializer: RouterStateSerializer, + @Inject(ROUTER_CONFIG) private config: StoreRouterConfig ) { + this.stateKey = this.config.stateKey as string; + this.setUpBeforePreactivationHook(); this.setUpStoreStateListener(); this.setUpStateRollbackEvents(); @@ -187,28 +252,28 @@ export class StoreRouterConnectingModule { this.store.subscribe(s => { this.storeState = s; }); - this.store.select('routerReducer').subscribe(() => { + this.store.select(this.stateKey).subscribe(() => { this.navigateIfNeeded(); }); } private shouldDispatchRouterNavigation(): boolean { - if (!this.storeState['routerReducer']) return true; + if (!this.storeState[this.stateKey]) return true; return !this.navigationTriggeredByDispatch; } private navigateIfNeeded(): void { if ( - !this.storeState['routerReducer'] || - !this.storeState['routerReducer'].state + !this.storeState[this.stateKey] || + !this.storeState[this.stateKey].state ) { return; } if (this.dispatchTriggeredByRouter) return; - if (this.router.url !== this.storeState['routerReducer'].state.url) { + if (this.router.url !== this.storeState[this.stateKey].state.url) { this.navigationTriggeredByDispatch = true; - this.router.navigateByUrl(this.storeState['routerReducer'].state.url); + this.router.navigateByUrl(this.storeState[this.stateKey].state.url); } } diff --git a/yarn.lock b/yarn.lock index 45aefddacf..a28e354c7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7873,4 +7873,4 @@ zone.js@^0.8.12: zone.js@^0.8.14: version "0.8.18" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.18.tgz#8cecb3977fcd1b3090562ff4570e2847e752b48d" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.18.tgz#8cecb3977fcd1b3090562ff4570e2847e752b48d" \ No newline at end of file