diff --git a/modules/router-store/spec/router_store_module.spec.ts b/modules/router-store/spec/router_store_module.spec.ts index c6220efb5c..2003529230 100644 --- a/modules/router-store/spec/router_store_module.spec.ts +++ b/modules/router-store/spec/router_store_module.spec.ts @@ -1,24 +1,24 @@ import { TestBed } from '@angular/core/testing'; -import { Router, RouterEvent, NavigationEnd } from '@angular/router'; +import { NavigationEnd, Router, RouterEvent } from '@angular/router'; import { + FullRouterStateSerializer, + MinimalRouterStateSerializer, + RouterAction, routerReducer, RouterReducerState, - StoreRouterConnectingModule, - RouterAction, RouterState, RouterStateSerializer, - MinimalRouterStateSerializer, - FullRouterStateSerializer, } from '@ngrx/router-store'; -import { select, Store, ActionsSubject } from '@ngrx/store'; -import { withLatestFrom, filter, skip } from 'rxjs/operators'; +import { ActionsSubject, select, Store } from '@ngrx/store'; +import { filter, withLatestFrom } from 'rxjs/operators'; import { createTestModule } from './utils'; +import { StoreRouterConnectingService } from '../src/store_router_connecting.service'; describe('Router Store Module', () => { describe('with defining state key', () => { const customStateKey = 'router-reducer'; - let storeRouterConnectingModule: StoreRouterConnectingModule; + let storeRouterConnectingService: StoreRouterConnectingService; let store: Store; let router: Router; @@ -38,11 +38,13 @@ describe('Router Store Module', () => { store = TestBed.inject(Store); router = TestBed.inject(Router); - storeRouterConnectingModule = TestBed.inject(StoreRouterConnectingModule); + storeRouterConnectingService = TestBed.inject( + StoreRouterConnectingService + ); }); it('should have custom state key as own property', () => { - expect((storeRouterConnectingModule).stateKey).toBe(customStateKey); + expect((storeRouterConnectingService).stateKey).toBe(customStateKey); }); it('should call navigateIfNeeded with args selected by custom state key', (done: any) => { @@ -54,7 +56,7 @@ describe('Router Store Module', () => { }); spyOn( - storeRouterConnectingModule, + storeRouterConnectingService, 'navigateIfNeeded' as never ).and.callThrough(); logs = []; @@ -63,7 +65,7 @@ describe('Router Store Module', () => { // and store emits its payload. router.navigateByUrl('/').then(() => { const actual = (( - storeRouterConnectingModule + storeRouterConnectingService )).navigateIfNeeded.calls.allArgs(); expect(actual.length).toBe(1); @@ -77,7 +79,7 @@ describe('Router Store Module', () => { const customStateKey = 'routerReducer'; const customStateSelector = (state: State) => state.routerReducer; - let storeRouterConnectingModule: StoreRouterConnectingModule; + let storeRouterConnectingService: StoreRouterConnectingService; let store: Store; let router: Router; @@ -97,11 +99,13 @@ describe('Router Store Module', () => { store = TestBed.inject(Store); router = TestBed.inject(Router); - storeRouterConnectingModule = TestBed.inject(StoreRouterConnectingModule); + storeRouterConnectingService = TestBed.inject( + StoreRouterConnectingService + ); }); it('should have same state selector as own property', () => { - expect((storeRouterConnectingModule).stateKey).toBe( + expect((storeRouterConnectingService).stateKey).toBe( customStateSelector ); }); @@ -115,7 +119,7 @@ describe('Router Store Module', () => { }); spyOn( - storeRouterConnectingModule, + storeRouterConnectingService, 'navigateIfNeeded' as never ).and.callThrough(); logs = []; @@ -124,7 +128,7 @@ describe('Router Store Module', () => { // and store emits its payload. router.navigateByUrl('/').then(() => { const actual = (( - storeRouterConnectingModule + storeRouterConnectingService )).navigateIfNeeded.calls.allArgs(); expect(actual.length).toBe(1); diff --git a/modules/router-store/src/index.ts b/modules/router-store/src/index.ts index e73640cbf6..5f7553bbf7 100644 --- a/modules/router-store/src/index.ts +++ b/modules/router-store/src/index.ts @@ -22,15 +22,15 @@ export { routerRequestAction, } from './actions'; export { routerReducer, RouterReducerState } from './reducer'; +export { StoreRouterConnectingModule } from './router_store_module'; export { StateKeyOrSelector, - StoreRouterConnectingModule, StoreRouterConfig, NavigationActionTiming, ROUTER_CONFIG, DEFAULT_ROUTER_FEATURENAME, RouterState, -} from './router_store_module'; +} from './router_store_config'; export { RouterStateSerializer, BaseRouterStoreState, @@ -45,3 +45,4 @@ export { MinimalRouterStateSerializer, } from './serializers/minimal_serializer'; export { getSelectors, createRouterSelector } from './router_selectors'; +export { provideRouterStore } from './provide_router_store'; diff --git a/modules/router-store/src/models.ts b/modules/router-store/src/models.ts index ade7dbe5ae..cd1531cbcb 100644 --- a/modules/router-store/src/models.ts +++ b/modules/router-store/src/models.ts @@ -1,4 +1,4 @@ -import { Params, Data } from '@angular/router'; +import { Data, Params } from '@angular/router'; import { MemoizedSelector } from '@ngrx/store'; export interface RouterStateSelectors { diff --git a/modules/router-store/src/provide_router_store.ts b/modules/router-store/src/provide_router_store.ts new file mode 100644 index 0000000000..a2fb846a67 --- /dev/null +++ b/modules/router-store/src/provide_router_store.ts @@ -0,0 +1,63 @@ +import { ENVIRONMENT_INITIALIZER, inject, Provider } from '@angular/core'; +import { + _createRouterConfig, + _ROUTER_CONFIG, + ROUTER_CONFIG, + RouterState, + StoreRouterConfig, +} from './router_store_config'; +import { + FullRouterStateSerializer, + SerializedRouterStateSnapshot, +} from './serializers/full_serializer'; +import { MinimalRouterStateSerializer } from './serializers/minimal_serializer'; +import { + BaseRouterStoreState, + RouterStateSerializer, +} from './serializers/base'; +import { StoreRouterConnectingService } from './store_router_connecting.service'; +import { EnvironmentProviders } from '@ngrx/store'; + +/** + * Connects the Angular Router to the Store. + * + * @usageNotes + * + * ```typescript + * bootstrapApplication(AppComponent, { + * providers: [ + * provideRouterStore() + * ] + * }) + * ``` + */ +export function provideRouterStore< + T extends BaseRouterStoreState = SerializedRouterStateSnapshot +>(config: StoreRouterConfig = {}): EnvironmentProviders { + return { + ɵproviders: [ + { provide: _ROUTER_CONFIG, useValue: config }, + { + provide: ROUTER_CONFIG, + useFactory: _createRouterConfig, + deps: [_ROUTER_CONFIG], + }, + { + provide: RouterStateSerializer, + useClass: config.serializer + ? config.serializer + : config.routerState === RouterState.Full + ? FullRouterStateSerializer + : MinimalRouterStateSerializer, + }, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory() { + return () => inject(StoreRouterConnectingService); + }, + }, + StoreRouterConnectingService, + ], + }; +} diff --git a/modules/router-store/src/router_selectors.ts b/modules/router-store/src/router_selectors.ts index a2857fd84a..08039a443d 100644 --- a/modules/router-store/src/router_selectors.ts +++ b/modules/router-store/src/router_selectors.ts @@ -5,7 +5,7 @@ import { } from '@ngrx/store'; import { RouterStateSelectors } from './models'; import { RouterReducerState } from './reducer'; -import { DEFAULT_ROUTER_FEATURENAME } from './router_store_module'; +import { DEFAULT_ROUTER_FEATURENAME } from './router_store_config'; export function createRouterSelector< State extends Record diff --git a/modules/router-store/src/router_store_config.ts b/modules/router-store/src/router_store_config.ts new file mode 100644 index 0000000000..af98c9fb41 --- /dev/null +++ b/modules/router-store/src/router_store_config.ts @@ -0,0 +1,67 @@ +import { InjectionToken } from '@angular/core'; +import { Selector } from '@ngrx/store'; +import { RouterReducerState } from './reducer'; +import { + BaseRouterStoreState, + RouterStateSerializer, +} from './serializers/base'; +import { SerializedRouterStateSnapshot } from './serializers/full_serializer'; +import { MinimalRouterStateSerializer } from './serializers/minimal_serializer'; + +export type StateKeyOrSelector< + T extends BaseRouterStoreState = SerializedRouterStateSnapshot +> = string | Selector>; + +export enum NavigationActionTiming { + PreActivation = 1, + PostActivation = 2, +} +export const DEFAULT_ROUTER_FEATURENAME = 'router'; + +export const _ROUTER_CONFIG = new InjectionToken( + '@ngrx/router-store Internal Configuration' +); +export const ROUTER_CONFIG = new InjectionToken( + '@ngrx/router-store Configuration' +); + +/** + * Minimal = Serializes the router event with MinimalRouterStateSerializer + * Full = Serializes the router event with FullRouterStateSerializer + */ +export const enum RouterState { + Full, + Minimal, +} + +export function _createRouterConfig( + config: StoreRouterConfig +): StoreRouterConfig { + return { + stateKey: DEFAULT_ROUTER_FEATURENAME, + serializer: MinimalRouterStateSerializer, + navigationActionTiming: NavigationActionTiming.PreActivation, + ...config, + }; +} + +export interface StoreRouterConfig< + T extends BaseRouterStoreState = SerializedRouterStateSnapshot +> { + stateKey?: StateKeyOrSelector; + serializer?: new (...args: any[]) => RouterStateSerializer; + /** + * By default, ROUTER_NAVIGATION is dispatched before guards and resolvers run. + * Therefore, the action could run too soon, for example + * there may be a navigation cancel due to a guard saying the navigation is not allowed. + * To run ROUTER_NAVIGATION after guards and resolvers, + * set this property to NavigationActionTiming.PostActivation. + */ + navigationActionTiming?: NavigationActionTiming; + /** + * Decides which router serializer should be used, if there is none provided, and the metadata on the dispatched @ngrx/router-store action payload. + * Set to `Minimal` to use the `MinimalRouterStateSerializer` and to set a minimal router event with the navigation id and url as payload. + * Set to `Full` to use the `FullRouterStateSerializer` and to set the angular router events as payload. + */ + routerState?: RouterState; +} diff --git a/modules/router-store/src/router_store_module.ts b/modules/router-store/src/router_store_module.ts index 3ee2d58bfc..a24e53844e 100644 --- a/modules/router-store/src/router_store_module.ts +++ b/modules/router-store/src/router_store_module.ts @@ -1,118 +1,8 @@ -import { - Inject, - InjectionToken, - ModuleWithProviders, - NgModule, - ErrorHandler, - isDevMode, -} from '@angular/core'; -import { - NavigationCancel, - NavigationError, - NavigationEnd, - Router, - RoutesRecognized, - NavigationStart, - Event, - RouterEvent, -} from '@angular/router'; -import { - isNgrxMockEnvironment, - RuntimeChecks, - select, - Selector, - Store, - ACTIVE_RUNTIME_CHECKS, -} from '@ngrx/store'; -import { withLatestFrom } from 'rxjs/operators'; - -import { - ROUTER_CANCEL, - ROUTER_ERROR, - ROUTER_NAVIGATED, - ROUTER_NAVIGATION, - ROUTER_REQUEST, -} from './actions'; -import { RouterReducerState } from './reducer'; -import { - RouterStateSerializer, - BaseRouterStoreState, -} from './serializers/base'; -import { - FullRouterStateSerializer, - SerializedRouterStateSnapshot, -} from './serializers/full_serializer'; -import { MinimalRouterStateSerializer } from './serializers/minimal_serializer'; - -export type StateKeyOrSelector< - T extends BaseRouterStoreState = SerializedRouterStateSnapshot -> = string | Selector>; - -/** - * Minimal = Serializes the router event with MinimalRouterStateSerializer - * Full = Serializes the router event with FullRouterStateSerializer - */ -export const enum RouterState { - Full, - Minimal, -} - -export interface StoreRouterConfig< - T extends BaseRouterStoreState = SerializedRouterStateSnapshot -> { - stateKey?: StateKeyOrSelector; - serializer?: new (...args: any[]) => RouterStateSerializer; - /** - * By default, ROUTER_NAVIGATION is dispatched before guards and resolvers run. - * Therefore, the action could run too soon, for example - * there may be a navigation cancel due to a guard saying the navigation is not allowed. - * To run ROUTER_NAVIGATION after guards and resolvers, - * set this property to NavigationActionTiming.PostActivation. - */ - navigationActionTiming?: NavigationActionTiming; - /** - * Decides which router serializer should be used, if there is none provided, and the metadata on the dispatched @ngrx/router-store action payload. - * Set to `Minimal` to use the `MinimalRouterStateSerializer` and to set a minimal router event with the navigation id and url as payload. - * Set to `Full` to use the `FullRouterStateSerializer` and to set the angular router events as payload. - */ - routerState?: RouterState; -} - -interface StoreRouterActionPayload { - event: RouterEvent; - routerState?: SerializedRouterStateSnapshot; - storeState?: any; -} - -export enum NavigationActionTiming { - PreActivation = 1, - PostActivation = 2, -} - -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 = 'router'; - -export function _createRouterConfig( - config: StoreRouterConfig -): StoreRouterConfig { - return { - stateKey: DEFAULT_ROUTER_FEATURENAME, - serializer: MinimalRouterStateSerializer, - navigationActionTiming: NavigationActionTiming.PreActivation, - ...config, - }; -} - -enum RouterTrigger { - NONE = 1, - ROUTER = 2, - STORE = 3, -} +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { BaseRouterStoreState } from './serializers/base'; +import { SerializedRouterStateSnapshot } from './serializers/full_serializer'; +import { StoreRouterConfig } from './router_store_config'; +import { provideRouterStore } from './provide_router_store'; /** * Connects RouterModule with StoreModule. @@ -158,12 +48,6 @@ enum RouterTrigger { */ @NgModule({}) export class StoreRouterConnectingModule { - private lastEvent: Event | null = null; - private routerState: SerializedRouterStateSnapshot | null = null; - private storeState: any; - private trigger = RouterTrigger.NONE; - private readonly stateKey: StateKeyOrSelector; - static forRoot< T extends BaseRouterStoreState = SerializedRouterStateSnapshot >( @@ -171,218 +55,7 @@ export class StoreRouterConnectingModule { ): ModuleWithProviders { return { ngModule: StoreRouterConnectingModule, - providers: [ - { provide: _ROUTER_CONFIG, useValue: config }, - { - provide: ROUTER_CONFIG, - useFactory: _createRouterConfig, - deps: [_ROUTER_CONFIG], - }, - { - provide: RouterStateSerializer, - useClass: config.serializer - ? config.serializer - : config.routerState === RouterState.Full - ? FullRouterStateSerializer - : MinimalRouterStateSerializer, - }, - ], + providers: [...provideRouterStore(config).ɵproviders], }; } - - constructor( - private store: Store, - private router: Router, - private serializer: RouterStateSerializer, - private errorHandler: ErrorHandler, - @Inject(ROUTER_CONFIG) private readonly config: StoreRouterConfig, - @Inject(ACTIVE_RUNTIME_CHECKS) - private readonly activeRuntimeChecks: RuntimeChecks - ) { - this.stateKey = this.config.stateKey as StateKeyOrSelector; - - if ( - !isNgrxMockEnvironment() && - isDevMode() && - (activeRuntimeChecks?.strictActionSerializability || - activeRuntimeChecks?.strictStateSerializability) && - this.serializer instanceof FullRouterStateSerializer - ) { - console.warn( - '@ngrx/router-store: The serializability runtime checks cannot be enabled ' + - 'with the FullRouterStateSerializer. The FullRouterStateSerializer ' + - 'has an unserializable router state and actions that are not serializable. ' + - 'To use the serializability runtime checks either use ' + - 'the MinimalRouterStateSerializer or implement a custom router state serializer.' - ); - } - - this.setUpStoreStateListener(); - this.setUpRouterEventsListener(); - } - - private setUpStoreStateListener(): void { - this.store - .pipe(select(this.stateKey as any), withLatestFrom(this.store)) - .subscribe(([routerStoreState, storeState]) => { - this.navigateIfNeeded(routerStoreState, storeState); - }); - } - - private navigateIfNeeded( - routerStoreState: RouterReducerState, - storeState: any - ): void { - if (!routerStoreState || !routerStoreState.state) { - return; - } - if (this.trigger === RouterTrigger.ROUTER) { - return; - } - if (this.lastEvent instanceof NavigationStart) { - return; - } - - const url = routerStoreState.state.url; - if (!isSameUrl(this.router.url, url)) { - this.storeState = storeState; - this.trigger = RouterTrigger.STORE; - this.router.navigateByUrl(url).catch((error) => { - this.errorHandler.handleError(error); - }); - } - } - - private setUpRouterEventsListener(): void { - const dispatchNavLate = - this.config.navigationActionTiming === - NavigationActionTiming.PostActivation; - let routesRecognized: RoutesRecognized; - - this.router.events - .pipe(withLatestFrom(this.store)) - .subscribe(([event, storeState]) => { - this.lastEvent = event; - - if (event instanceof NavigationStart) { - this.routerState = this.serializer.serialize( - this.router.routerState.snapshot - ); - if (this.trigger !== RouterTrigger.STORE) { - this.storeState = storeState; - this.dispatchRouterRequest(event); - } - } else if (event instanceof RoutesRecognized) { - routesRecognized = event; - - if (!dispatchNavLate && this.trigger !== RouterTrigger.STORE) { - this.dispatchRouterNavigation(event); - } - } else if (event instanceof NavigationCancel) { - this.dispatchRouterCancel(event); - this.reset(); - } else if (event instanceof NavigationError) { - this.dispatchRouterError(event); - this.reset(); - } else if (event instanceof NavigationEnd) { - if (this.trigger !== RouterTrigger.STORE) { - if (dispatchNavLate) { - this.dispatchRouterNavigation(routesRecognized); - } - this.dispatchRouterNavigated(event); - } - this.reset(); - } - }); - } - - private dispatchRouterRequest(event: NavigationStart): void { - this.dispatchRouterAction(ROUTER_REQUEST, { event }); - } - - private dispatchRouterNavigation( - lastRoutesRecognized: RoutesRecognized - ): void { - const nextRouterState = this.serializer.serialize( - lastRoutesRecognized.state - ); - this.dispatchRouterAction(ROUTER_NAVIGATION, { - routerState: nextRouterState, - event: new RoutesRecognized( - lastRoutesRecognized.id, - lastRoutesRecognized.url, - lastRoutesRecognized.urlAfterRedirects, - nextRouterState - ), - }); - } - - private dispatchRouterCancel(event: NavigationCancel): void { - this.dispatchRouterAction(ROUTER_CANCEL, { - storeState: this.storeState, - event, - }); - } - - private dispatchRouterError(event: NavigationError): void { - this.dispatchRouterAction(ROUTER_ERROR, { - storeState: this.storeState, - event: new NavigationError(event.id, event.url, `${event}`), - }); - } - - private dispatchRouterNavigated(event: NavigationEnd): void { - const routerState = this.serializer.serialize( - this.router.routerState.snapshot - ); - this.dispatchRouterAction(ROUTER_NAVIGATED, { event, routerState }); - } - - private dispatchRouterAction( - type: string, - payload: StoreRouterActionPayload - ): void { - this.trigger = RouterTrigger.ROUTER; - try { - this.store.dispatch({ - type, - payload: { - routerState: this.routerState, - ...payload, - event: - this.config.routerState === RouterState.Full - ? payload.event - : { - id: payload.event.id, - url: payload.event.url, - // safe, as it will just be `undefined` for non-NavigationEnd router events - urlAfterRedirects: (payload.event as NavigationEnd) - .urlAfterRedirects, - }, - }, - }); - } finally { - this.trigger = RouterTrigger.NONE; - } - } - - private reset() { - this.trigger = RouterTrigger.NONE; - this.storeState = null; - this.routerState = null; - } -} - -/** - * Check if the URLs are matching. Accounts for the possibility of trailing "/" in url. - */ -function isSameUrl(first: string, second: string): boolean { - return stripTrailingSlash(first) === stripTrailingSlash(second); -} - -function stripTrailingSlash(text: string): string { - if (text?.length > 0 && text[text.length - 1] === '/') { - return text.substring(0, text.length - 1); - } - return text; } diff --git a/modules/router-store/src/serializers/minimal_serializer.ts b/modules/router-store/src/serializers/minimal_serializer.ts index 75adfeb7c8..e68e248ac3 100644 --- a/modules/router-store/src/serializers/minimal_serializer.ts +++ b/modules/router-store/src/serializers/minimal_serializer.ts @@ -1,4 +1,4 @@ -import { RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { BaseRouterStoreState, RouterStateSerializer } from './base'; export interface MinimalActivatedRouteSnapshot { diff --git a/modules/router-store/src/store_router_connecting.service.ts b/modules/router-store/src/store_router_connecting.service.ts new file mode 100644 index 0000000000..8c4bd45ed7 --- /dev/null +++ b/modules/router-store/src/store_router_connecting.service.ts @@ -0,0 +1,260 @@ +import { ErrorHandler, Inject, Injectable, isDevMode } from '@angular/core'; +import { + Event, + NavigationCancel, + NavigationEnd, + NavigationError, + NavigationStart, + Router, + RouterEvent, + RoutesRecognized, +} from '@angular/router'; +import { + ACTIVE_RUNTIME_CHECKS, + isNgrxMockEnvironment, + RuntimeChecks, + select, + Store, +} from '@ngrx/store'; +import { withLatestFrom } from 'rxjs/operators'; +import { + ROUTER_CANCEL, + ROUTER_ERROR, + ROUTER_NAVIGATED, + ROUTER_NAVIGATION, + ROUTER_REQUEST, +} from './actions'; +import { + NavigationActionTiming, + ROUTER_CONFIG, + RouterState, + StateKeyOrSelector, + StoreRouterConfig, +} from './router_store_config'; +import { + FullRouterStateSerializer, + SerializedRouterStateSnapshot, +} from './serializers/full_serializer'; +import { RouterReducerState } from './reducer'; +import { RouterStateSerializer } from './serializers/base'; + +enum RouterTrigger { + NONE = 1, + ROUTER = 2, + STORE = 3, +} + +interface StoreRouterActionPayload { + event: RouterEvent; + routerState?: SerializedRouterStateSnapshot; + storeState?: any; +} + +/** + * Shared router initialization logic used alongside both the StoreRouterConnectingModule and the provideRouterStore + * function + */ +@Injectable() +export class StoreRouterConnectingService { + private lastEvent: Event | null = null; + private routerState: SerializedRouterStateSnapshot | null = null; + private storeState: any; + private trigger = RouterTrigger.NONE; + private readonly stateKey: StateKeyOrSelector; + + constructor( + private store: Store, + private router: Router, + private serializer: RouterStateSerializer, + private errorHandler: ErrorHandler, + @Inject(ROUTER_CONFIG) private readonly config: StoreRouterConfig, + @Inject(ACTIVE_RUNTIME_CHECKS) + private readonly activeRuntimeChecks: RuntimeChecks + ) { + this.stateKey = this.config.stateKey as StateKeyOrSelector; + + if ( + !isNgrxMockEnvironment() && + isDevMode() && + (activeRuntimeChecks?.strictActionSerializability || + activeRuntimeChecks?.strictStateSerializability) && + this.serializer instanceof FullRouterStateSerializer + ) { + console.warn( + '@ngrx/router-store: The serializability runtime checks cannot be enabled ' + + 'with the FullRouterStateSerializer. The FullRouterStateSerializer ' + + 'has an unserializable router state and actions that are not serializable. ' + + 'To use the serializability runtime checks either use ' + + 'the MinimalRouterStateSerializer or implement a custom router state serializer.' + ); + } + + this.setUpStoreStateListener(); + this.setUpRouterEventsListener(); + } + + private setUpStoreStateListener(): void { + this.store + .pipe(select(this.stateKey as any), withLatestFrom(this.store)) + .subscribe(([routerStoreState, storeState]) => { + this.navigateIfNeeded(routerStoreState, storeState); + }); + } + + private navigateIfNeeded( + routerStoreState: RouterReducerState, + storeState: any + ): void { + if (!routerStoreState || !routerStoreState.state) { + return; + } + if (this.trigger === RouterTrigger.ROUTER) { + return; + } + if (this.lastEvent instanceof NavigationStart) { + return; + } + + const url = routerStoreState.state.url; + if (!isSameUrl(this.router.url, url)) { + this.storeState = storeState; + this.trigger = RouterTrigger.STORE; + this.router.navigateByUrl(url).catch((error) => { + this.errorHandler.handleError(error); + }); + } + } + + private setUpRouterEventsListener(): void { + const dispatchNavLate = + this.config.navigationActionTiming === + NavigationActionTiming.PostActivation; + let routesRecognized: RoutesRecognized; + + this.router.events + .pipe(withLatestFrom(this.store)) + .subscribe(([event, storeState]) => { + this.lastEvent = event; + + if (event instanceof NavigationStart) { + this.routerState = this.serializer.serialize( + this.router.routerState.snapshot + ); + if (this.trigger !== RouterTrigger.STORE) { + this.storeState = storeState; + this.dispatchRouterRequest(event); + } + } else if (event instanceof RoutesRecognized) { + routesRecognized = event; + + if (!dispatchNavLate && this.trigger !== RouterTrigger.STORE) { + this.dispatchRouterNavigation(event); + } + } else if (event instanceof NavigationCancel) { + this.dispatchRouterCancel(event); + this.reset(); + } else if (event instanceof NavigationError) { + this.dispatchRouterError(event); + this.reset(); + } else if (event instanceof NavigationEnd) { + if (this.trigger !== RouterTrigger.STORE) { + if (dispatchNavLate) { + this.dispatchRouterNavigation(routesRecognized); + } + this.dispatchRouterNavigated(event); + } + this.reset(); + } + }); + } + + private dispatchRouterRequest(event: NavigationStart): void { + this.dispatchRouterAction(ROUTER_REQUEST, { event }); + } + + private dispatchRouterNavigation( + lastRoutesRecognized: RoutesRecognized + ): void { + const nextRouterState = this.serializer.serialize( + lastRoutesRecognized.state + ); + this.dispatchRouterAction(ROUTER_NAVIGATION, { + routerState: nextRouterState, + event: new RoutesRecognized( + lastRoutesRecognized.id, + lastRoutesRecognized.url, + lastRoutesRecognized.urlAfterRedirects, + nextRouterState + ), + }); + } + + private dispatchRouterCancel(event: NavigationCancel): void { + this.dispatchRouterAction(ROUTER_CANCEL, { + storeState: this.storeState, + event, + }); + } + + private dispatchRouterError(event: NavigationError): void { + this.dispatchRouterAction(ROUTER_ERROR, { + storeState: this.storeState, + event: new NavigationError(event.id, event.url, `${event}`), + }); + } + + private dispatchRouterNavigated(event: NavigationEnd): void { + const routerState = this.serializer.serialize( + this.router.routerState.snapshot + ); + this.dispatchRouterAction(ROUTER_NAVIGATED, { event, routerState }); + } + + private dispatchRouterAction( + type: string, + payload: StoreRouterActionPayload + ): void { + this.trigger = RouterTrigger.ROUTER; + try { + this.store.dispatch({ + type, + payload: { + routerState: this.routerState, + ...payload, + event: + this.config.routerState === RouterState.Full + ? payload.event + : { + id: payload.event.id, + url: payload.event.url, + // safe, as it will just be `undefined` for non-NavigationEnd router events + urlAfterRedirects: (payload.event as NavigationEnd) + .urlAfterRedirects, + }, + }, + }); + } finally { + this.trigger = RouterTrigger.NONE; + } + } + + private reset() { + this.trigger = RouterTrigger.NONE; + this.storeState = null; + this.routerState = null; + } +} + +/** + * Check if the URLs are matching. Accounts for the possibility of trailing "/" in url. + */ +function isSameUrl(first: string, second: string): boolean { + return stripTrailingSlash(first) === stripTrailingSlash(second); +} + +function stripTrailingSlash(text: string): string { + if (text?.length > 0 && text[text.length - 1] === '/') { + return text.substring(0, text.length - 1); + } + return text; +}