11import { inject , Injectable , Provider } from '@angular/core' ;
2- import {
3- Navigation ,
4- NavigationEnd ,
5- NavigationStart ,
6- Router ,
7- } from '@angular/router' ;
2+ import { NavigationEnd , NavigationStart , Router } from '@angular/router' ;
83import { ComponentStore , provideComponentStore } from '@ngrx/component-store' ;
9- import { concatMap , filter , Observable , take } from 'rxjs' ;
10-
11- interface RouterHistoryRecord {
12- readonly id : number ;
13- readonly url : string ;
14- }
4+ import { filter , Observable } from 'rxjs' ;
155
166interface RouterHistoryState {
17- readonly currentIndex : number ;
18- readonly event ?: NavigationStart | NavigationEnd ;
19- readonly history : readonly RouterHistoryRecord [ ] ;
20- readonly id : number ;
21- readonly idToRestore ?: number ;
22- readonly trigger ?: Navigation [ 'trigger' ] ;
7+ readonly history : NavigationHistory ;
238}
249
10+ type CompleteNavigation = readonly [ NavigationStart , NavigationEnd ] ;
11+ type NavigationHistory = Record < number , NavigationSequence > ;
12+ type NavigationSequence = PendingNavigation | CompleteNavigation ;
13+ type PendingNavigation = readonly [ NavigationStart ] ;
14+
2515export function provideRouterHistoryStore ( ) : Provider [ ] {
2616 return [ provideComponentStore ( RouterHistoryStore ) ] ;
2717}
@@ -59,11 +49,8 @@ export function provideRouterHistoryStore(): Provider[] {
5949export class RouterHistoryStore extends ComponentStore < RouterHistoryState > {
6050 #router = inject ( Router ) ;
6151
62- #currentIndex$: Observable < number > = this . select (
63- ( state ) => state . currentIndex
64- ) ;
65- #history$: Observable < readonly RouterHistoryRecord [ ] > = this . select (
66- ( state ) => state . history
52+ #history$ = this . select ( ( state ) => state . history ) . pipe (
53+ filter ( ( history ) => Object . keys ( history ) . length > 0 )
6754 ) ;
6855 #navigationEnd$: Observable < NavigationEnd > = this . #router. events . pipe (
6956 filter ( ( event ) : event is NavigationEnd => event instanceof NavigationEnd )
@@ -73,43 +60,49 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
7360 ( event ) : event is NavigationStart => event instanceof NavigationStart
7461 )
7562 ) ;
76- #imperativeNavigationEnd$: Observable < NavigationEnd > =
77- this . #navigationStart$. pipe (
78- filter ( ( event ) => event . navigationTrigger === 'imperative' ) ,
79- concatMap ( ( ) => this . #navigationEnd$. pipe ( take ( 1 ) ) )
80- ) ;
81- #popstateNavigationEnd$: Observable < NavigationEnd > =
82- this . #navigationStart$. pipe (
83- filter ( ( event ) => event . navigationTrigger === 'popstate' ) ,
84- concatMap ( ( ) => this . #navigationEnd$. pipe ( take ( 1 ) ) )
85- ) ;
8663
87- currentUrl$ : Observable < string > = this . select (
88- this . #navigationEnd$. pipe (
89- concatMap ( ( ) =>
90- this . select (
91- this . #currentIndex$,
92- this . #history$,
93- ( currentIndex , history ) => [ currentIndex , history ] as const
94- )
95- )
96- ) ,
97- ( [ currentIndex , history ] ) => history [ currentIndex ] . url ,
64+ #maxCompletedNavigationId$ = this . select ( this . #history$, ( history ) =>
65+ Number (
66+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67+ Object . entries ( history )
68+ . reverse ( )
69+ . find ( ( [ , navigation ] ) => navigation . length === 2 ) ! [ 0 ]
70+ )
71+ ) ;
72+ #latestCompletedNavigation$ = this . select (
73+ this . #maxCompletedNavigationId$,
74+ this . #history$,
75+ ( maxCompletedNavigationId , history ) =>
76+ history [ maxCompletedNavigationId ] as CompleteNavigation ,
9877 {
9978 debounce : true ,
10079 }
10180 ) ;
102- previousUrl$ : Observable < string | null > = this . select (
103- this . #navigationEnd$. pipe (
104- concatMap ( ( ) =>
105- this . select (
106- this . #currentIndex$,
107- this . #history$,
108- ( currentIndex , history ) => [ currentIndex , history ] as const
109- )
110- )
111- ) ,
112- ( [ currentIndex , history ] ) => history [ currentIndex - 1 ] ?. url ?? null ,
81+
82+ currentUrl$ : Observable < string > = this . select (
83+ this . #latestCompletedNavigation$,
84+ ( [ , end ] ) => end . urlAfterRedirects
85+ ) ;
86+ previousUrl$ : Observable < string | undefined > = this . select (
87+ this . #history$,
88+ this . #maxCompletedNavigationId$,
89+ ( history , maxCompletedNavigationId ) => {
90+ if ( maxCompletedNavigationId === 1 ) {
91+ return undefined ;
92+ }
93+
94+ const [ completedNavigationSourceStart ] = this . #getNavigationSource(
95+ maxCompletedNavigationId ,
96+ history
97+ ) ;
98+ const previousNavigationId = completedNavigationSourceStart . id - 1 ;
99+ const [ , previousNavigationSourceEnd ] = this . #getNavigationSource(
100+ previousNavigationId ,
101+ history
102+ ) ;
103+
104+ return previousNavigationSourceEnd . urlAfterRedirects ;
105+ } ,
113106 {
114107 debounce : true ,
115108 }
@@ -118,95 +111,49 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
118111 constructor ( ) {
119112 super ( initialState ) ;
120113
121- this . #updateRouterHistoryOnNavigationStart( this . #navigationStart$) ;
122- this . #updateRouterHistoryOnImperativeNavigationEnd(
123- this . #imperativeNavigationEnd$
124- ) ;
125- this . #updateRouterHistoryOnPopstateNavigationEnd(
126- this . #popstateNavigationEnd$
127- ) ;
114+ this . #addNavigationStart( this . #navigationStart$) ;
115+ this . #addNavigationEnd( this . #navigationEnd$) ;
128116 }
129117
130- /**
131- * Update router history on imperative navigation end (`Router#navigate`,
132- * `Router#navigateByUrl`, or `RouterLink` click).
133- */
134- #updateRouterHistoryOnImperativeNavigationEnd = this . updater < NavigationEnd > (
135- ( state , event ) : RouterHistoryState => {
136- let currentIndex = state . currentIndex ;
137- let history = state . history ;
138- // remove all events in history that come after the current index
139- history = [
140- ...history . slice ( 0 , currentIndex + 1 ) ,
141- // add the new event to the end of the history
142- {
143- id : state . id ,
144- url : event . urlAfterRedirects ,
145- } ,
146- ] ;
147- // set the new event as our current history index
148- currentIndex = history . length - 1 ;
149-
150- return {
151- ...state ,
152- currentIndex,
153- event,
154- history,
155- } ;
156- }
118+ #addNavigationEnd = this . updater < NavigationEnd > (
119+ ( state , event ) : RouterHistoryState => ( {
120+ ...state ,
121+ history : {
122+ ...state . history ,
123+ [ event . id ] : [ state . history [ event . id ] [ 0 ] , event ] ,
124+ } ,
125+ } )
157126 ) ;
158127
159- #updateRouterHistoryOnNavigationStart = this . updater < NavigationStart > (
128+ #addNavigationStart = this . updater < NavigationStart > (
160129 ( state , event ) : RouterHistoryState => ( {
161130 ...state ,
162- id : event . id ,
163- idToRestore : event . restoredState ?. navigationId ?? undefined ,
164- event,
165- trigger : event . navigationTrigger ,
131+ history : {
132+ ... state . history ,
133+ [ event . id ] : [ event ] ,
134+ } ,
166135 } )
167136 ) ;
168137
169- /**
170- * Update router history on browser navigation end (back, forward, and other
171- * `popstate` or `pushstate` events).
172- */
173- #updateRouterHistoryOnPopstateNavigationEnd = this . updater < NavigationEnd > (
174- ( state , event ) : RouterHistoryState => {
175- let currentIndex = 0 ;
176- let { history } = state ;
177- // get the history item that references the idToRestore
178- const historyIndexToRestore = history . findIndex (
179- ( historyRecord ) => historyRecord . id === state . idToRestore
180- ) ;
181-
182- // if found, set the current index to that history item and update the id
183- if ( historyIndexToRestore > - 1 ) {
184- currentIndex = historyIndexToRestore ;
185- history = [
186- ...history . slice ( 0 , historyIndexToRestore ) ,
187- {
188- ...history [ historyIndexToRestore ] ,
189- id : state . id ,
190- } ,
191- ...history . slice ( historyIndexToRestore + 1 ) ,
138+ #getNavigationSource(
139+ navigationId : number ,
140+ history : NavigationHistory
141+ ) : CompleteNavigation {
142+ let navigation = history [ navigationId ] ;
143+
144+ while ( navigation [ 0 ] . navigationTrigger === 'popstate' ) {
145+ navigation =
146+ history [
147+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148+ navigation [ 0 ] . restoredState ! . navigationId
192149 ] ;
193- }
194-
195- return {
196- ...state ,
197- currentIndex,
198- event,
199- history,
200- } ;
150+ navigationId = navigation [ 0 ] . id ;
201151 }
202- ) ;
152+
153+ return navigation as CompleteNavigation ;
154+ }
203155}
204156
205157export const initialState : RouterHistoryState = {
206- currentIndex : 0 ,
207- event : undefined ,
208158 history : [ ] ,
209- id : 0 ,
210- idToRestore : 0 ,
211- trigger : undefined ,
212159} ;
0 commit comments