88
99import { Injector , NgModuleRef } from '@angular/core' ;
1010import { EmptyError , from , Observable , Observer , of } from 'rxjs' ;
11- import { catchError , combineAll , concatMap , first , map , mergeMap , tap } from 'rxjs/operators' ;
11+ import { catchError , concatMap , first , last , map , mergeMap , scan , tap } from 'rxjs/operators' ;
1212
1313import { LoadedRouterConfig , Route , Routes } from './config' ;
1414import { CanLoadFn } from './interfaces' ;
1515import { prioritizedGuardValue } from './operators/prioritized_guard_value' ;
1616import { RouterConfigLoader } from './router_config_loader' ;
1717import { navigationCancelingError , Params , PRIMARY_OUTLET } from './shared' ;
1818import { UrlSegment , UrlSegmentGroup , UrlSerializer , UrlTree } from './url_tree' ;
19- import { forEach , waitForMap , wrapIntoObservable } from './utils/collection' ;
20- import { getOutlet , groupRoutesByOutlet } from './utils/config' ;
21- import { match , noLeftoversInUrl , split } from './utils/config_matching' ;
19+ import { forEach , wrapIntoObservable } from './utils/collection' ;
20+ import { getOutlet , sortByMatchingOutlets } from './utils/config' ;
21+ import { isImmediateMatch , match , noLeftoversInUrl , split } from './utils/config_matching' ;
2222import { isCanLoad , isFunction , isUrlTree } from './utils/type_guards' ;
2323
2424class NoMatch {
@@ -78,8 +78,17 @@ class ApplyRedirects {
7878 }
7979
8080 apply ( ) : Observable < UrlTree > {
81+ const splitGroup = split ( this . urlTree . root , [ ] , [ ] , this . config ) . segmentGroup ;
82+ // TODO(atscott): creating a new segment removes the _sourceSegment _segmentIndexShift, which is
83+ // only necessary to prevent failures in tests which assert exact object matches. The `split` is
84+ // now shared between `applyRedirects` and `recognize` but only the `recognize` step needs these
85+ // properties. Before the implementations were merged, the `applyRedirects` would not assign
86+ // them. We should be able to remove this logic as a "breaking change" but should do some more
87+ // investigation into the failures first.
88+ const rootSegmentGroup = new UrlSegmentGroup ( splitGroup . segments , splitGroup . children ) ;
89+
8190 const expanded$ =
82- this . expandSegmentGroup ( this . ngModule , this . config , this . urlTree . root , PRIMARY_OUTLET ) ;
91+ this . expandSegmentGroup ( this . ngModule , this . config , rootSegmentGroup , PRIMARY_OUTLET ) ;
8392 const urlTrees$ = expanded$ . pipe ( map ( ( rootSegmentGroup : UrlSegmentGroup ) => {
8493 return this . createUrlTree (
8594 squashSegmentGroup ( rootSegmentGroup ) , this . urlTree . queryParams , this . urlTree . fragment ! ) ;
@@ -143,74 +152,73 @@ class ApplyRedirects {
143152 private expandChildren (
144153 ngModule : NgModuleRef < any > , routes : Route [ ] ,
145154 segmentGroup : UrlSegmentGroup ) : Observable < { [ name : string ] : UrlSegmentGroup } > {
146- return waitForMap (
147- segmentGroup . children ,
148- ( childOutlet , child ) => this . expandSegmentGroup ( ngModule , routes , child , childOutlet ) ) ;
155+ // Expand outlets one at a time, starting with the primary outlet. We need to do it this way
156+ // because an absolute redirect from the primary outlet takes precedence.
157+ const childOutlets : string [ ] = [ ] ;
158+ for ( const child of Object . keys ( segmentGroup . children ) ) {
159+ if ( child === 'primary' ) {
160+ childOutlets . unshift ( child ) ;
161+ } else {
162+ childOutlets . push ( child ) ;
163+ }
164+ }
165+
166+ return from ( childOutlets )
167+ . pipe (
168+ concatMap ( childOutlet => {
169+ const child = segmentGroup . children [ childOutlet ] ;
170+ // Sort the routes so routes with outlets that match the the segment appear
171+ // first, followed by routes for other outlets, which might match if they have an
172+ // empty path.
173+ const sortedRoutes = sortByMatchingOutlets ( routes , childOutlet ) ;
174+ return this . expandSegmentGroup ( ngModule , sortedRoutes , child , childOutlet )
175+ . pipe ( map ( s => ( { segment : s , outlet : childOutlet } ) ) ) ;
176+ } ) ,
177+ scan (
178+ ( children , expandedChild ) => {
179+ children [ expandedChild . outlet ] = expandedChild . segment ;
180+ return children ;
181+ } ,
182+ { } as { [ outlet : string ] : UrlSegmentGroup } ) ,
183+ last ( ) ,
184+ ) ;
149185 }
150186
151187 private expandSegment (
152188 ngModule : NgModuleRef < any > , segmentGroup : UrlSegmentGroup , routes : Route [ ] ,
153189 segments : UrlSegment [ ] , outlet : string ,
154190 allowRedirects : boolean ) : Observable < UrlSegmentGroup > {
155- // We need to expand each outlet group independently to ensure that we not only load modules
156- // for routes matching the given `outlet`, but also those which will be activated because
157- // their path is empty string. This can result in multiple outlets being activated at once.
158- const routesByOutlet : Map < string , Route [ ] > = groupRoutesByOutlet ( routes ) ;
159- if ( ! routesByOutlet . has ( outlet ) ) {
160- routesByOutlet . set ( outlet , [ ] ) ;
161- }
162-
163- const expandRoutes = ( routes : Route [ ] ) => {
164- return from ( routes ) . pipe (
165- concatMap ( ( r : Route ) => {
166- const expanded$ = this . expandSegmentAgainstRoute (
167- ngModule , segmentGroup , routes , r , segments , outlet , allowRedirects ) ;
168- return expanded$ . pipe ( catchError ( e => {
169- if ( e instanceof NoMatch ) {
170- return of ( null ) ;
171- }
172- throw e ;
173- } ) ) ;
174- } ) ,
175- first ( ( s : UrlSegmentGroup | null ) : s is UrlSegmentGroup => s !== null ) ,
176- catchError ( e => {
177- if ( e instanceof EmptyError || e . name === 'EmptyError' ) {
178- if ( noLeftoversInUrl ( segmentGroup , segments , outlet ) ) {
179- return of ( new UrlSegmentGroup ( [ ] , { } ) ) ;
180- }
181- throw new NoMatch ( segmentGroup ) ;
191+ return from ( routes ) . pipe (
192+ concatMap ( ( r : any ) => {
193+ const expanded$ = this . expandSegmentAgainstRoute (
194+ ngModule , segmentGroup , routes , r , segments , outlet , allowRedirects ) ;
195+ return expanded$ . pipe ( catchError ( ( e : any ) => {
196+ if ( e instanceof NoMatch ) {
197+ return of ( null ) ;
182198 }
183199 throw e ;
184- } ) ,
185- ) ;
186- } ;
187-
188- const expansions = Array . from ( routesByOutlet . entries ( ) ) . map ( ( [ routeOutlet , routes ] ) => {
189- const expanded = expandRoutes ( routes ) ;
190- // Map all results from outlets we aren't activating to `null` so they can be ignored later
191- return routeOutlet === outlet ? expanded :
192- expanded . pipe ( map ( ( ) => null ) , catchError ( ( ) => of ( null ) ) ) ;
193- } ) ;
194- return from ( expansions )
195- . pipe (
196- combineAll ( ) ,
197- first ( ) ,
198- // Return only the expansion for the route outlet we are trying to activate.
199- map ( results => results . find ( result => result !== null ) ! ) ,
200- ) ;
200+ } ) ) ;
201+ } ) ,
202+ first ( ( s ) : s is UrlSegmentGroup => ! ! s ) , catchError ( ( e : any , _ : any ) => {
203+ if ( e instanceof EmptyError || e . name === 'EmptyError' ) {
204+ if ( noLeftoversInUrl ( segmentGroup , segments , outlet ) ) {
205+ return of ( new UrlSegmentGroup ( [ ] , { } ) ) ;
206+ }
207+ throw new NoMatch ( segmentGroup ) ;
208+ }
209+ throw e ;
210+ } ) ) ;
201211 }
202212
203213 private expandSegmentAgainstRoute (
204214 ngModule : NgModuleRef < any > , segmentGroup : UrlSegmentGroup , routes : Route [ ] , route : Route ,
205215 paths : UrlSegment [ ] , outlet : string , allowRedirects : boolean ) : Observable < UrlSegmentGroup > {
206- // Empty string segments are special because multiple outlets can match a single path, i.e.
207- // `[{path: '', component: B}, {path: '', loadChildren: () => {}, outlet: "about"}]`
208- if ( getOutlet ( route ) !== outlet && route . path !== '' ) {
216+ if ( ! isImmediateMatch ( route , segmentGroup , paths , outlet ) ) {
209217 return noMatch ( segmentGroup ) ;
210218 }
211219
212220 if ( route . redirectTo === undefined ) {
213- return this . matchSegmentAgainstRoute ( ngModule , segmentGroup , route , paths ) ;
221+ return this . matchSegmentAgainstRoute ( ngModule , segmentGroup , route , paths , outlet ) ;
214222 }
215223
216224 if ( allowRedirects && this . allowRedirects ) {
@@ -269,7 +277,7 @@ class ApplyRedirects {
269277
270278 private matchSegmentAgainstRoute (
271279 ngModule : NgModuleRef < any > , rawSegmentGroup : UrlSegmentGroup , route : Route ,
272- segments : UrlSegment [ ] ) : Observable < UrlSegmentGroup > {
280+ segments : UrlSegment [ ] , outlet : string ) : Observable < UrlSegmentGroup > {
273281 if ( route . path === '**' ) {
274282 if ( route . loadChildren ) {
275283 return this . configLoader . load ( ngModule . injector , route )
@@ -292,16 +300,11 @@ class ApplyRedirects {
292300 const childModule = routerConfig . module ;
293301 const childConfig = routerConfig . routes ;
294302
295- const { segmentGroup, slicedSegments} =
303+ const { segmentGroup : splitSegmentGroup , slicedSegments} =
296304 split ( rawSegmentGroup , consumedSegments , rawSlicedSegments , childConfig ) ;
297- // TODO(atscott): clearing the source segment and segment index shift is only necessary to
298- // prevent failures in tests which assert exact object matches. The `split` is now shared
299- // between applyRedirects and recognize and only the `recognize` step needs these properties.
300- // Before the implementations were merged, the applyRedirects would not assign them.
301- // We should be able to remove this logic as a "breaking change" but should do some more
302- // investigation into the failures first.
303- segmentGroup . _sourceSegment = undefined ;
304- segmentGroup . _segmentIndexShift = undefined ;
305+ // See comment on the other call to `split` about why this is necessary.
306+ const segmentGroup =
307+ new UrlSegmentGroup ( splitSegmentGroup . segments , splitSegmentGroup . children ) ;
305308
306309 if ( slicedSegments . length === 0 && segmentGroup . hasChildren ( ) ) {
307310 const expanded$ = this . expandChildren ( childModule , childConfig , segmentGroup ) ;
@@ -313,8 +316,10 @@ class ApplyRedirects {
313316 return of ( new UrlSegmentGroup ( consumedSegments , { } ) ) ;
314317 }
315318
319+ const matchedOnOutlet = getOutlet ( route ) === outlet ;
316320 const expanded$ = this . expandSegment (
317- childModule , segmentGroup , childConfig , slicedSegments , PRIMARY_OUTLET , true ) ;
321+ childModule , segmentGroup , childConfig , slicedSegments ,
322+ matchedOnOutlet ? PRIMARY_OUTLET : outlet , true ) ;
318323 return expanded$ . pipe (
319324 map ( ( cs : UrlSegmentGroup ) =>
320325 new UrlSegmentGroup ( consumedSegments . concat ( cs . segments ) , cs . children ) ) ) ;
@@ -473,7 +478,6 @@ class ApplyRedirects {
473478 }
474479}
475480
476-
477481/**
478482 * When possible, merges the primary outlet child into the parent `UrlSegmentGroup`.
479483 *
0 commit comments