@@ -89,20 +89,42 @@ export class Recognizer {
8989 return this . processSegment ( config , segmentGroup , segmentGroup . segments , outlet ) ;
9090 }
9191
92+ /**
93+ * Matches every child outlet in the `segmentGroup` to a `Route` in the config. Returns `null` if
94+ * we cannot find a match for _any_ of the children.
95+ *
96+ * @param config - The `Routes` to match against
97+ * @param segmentGroup - The `UrlSegmentGroup` whose children need to be matched against the
98+ * config.
99+ */
92100 processChildren ( config : Route [ ] , segmentGroup : UrlSegmentGroup ) :
93101 TreeNode < ActivatedRouteSnapshot > [ ] | null {
94102 const children : Array < TreeNode < ActivatedRouteSnapshot > > = [ ] ;
95103 for ( const childOutlet of Object . keys ( segmentGroup . children ) ) {
96104 const child = segmentGroup . children [ childOutlet ] ;
97- const outletChildren = this . processSegmentGroup ( config , child , childOutlet ) ;
105+ // Sort the config so that routes with outlets that match the one being activated appear
106+ // first, followed by routes for other outlets, which might match if they have an empty path.
107+ const sortedConfig = config . filter ( r => getOutlet ( r ) === childOutlet ) ;
108+ sortedConfig . push ( ...config . filter ( r => getOutlet ( r ) !== childOutlet ) ) ;
109+ const outletChildren = this . processSegmentGroup ( sortedConfig , child , childOutlet ) ;
98110 if ( outletChildren === null ) {
111+ // Configs must match all segment children so because we did not find a match for this
112+ // outlet, return `null`.
99113 return null ;
100114 }
101115 children . push ( ...outletChildren ) ;
102116 }
103- checkOutletNameUniqueness ( children ) ;
104- sortActivatedRouteSnapshots ( children ) ;
105- return children ;
117+ // Because we may have matched two outlets to the same empty path segment, we can have multiple
118+ // activated results for the same outlet. We should merge the children of these results so the
119+ // final return value is only one `TreeNode` per outlet.
120+ const mergedChildren = mergeEmptyPathMatches ( children ) ;
121+ if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
122+ // This should really never happen - we are only taking the first match for each outlet and
123+ // merge the empty path matches.
124+ checkOutletNameUniqueness ( mergedChildren ) ;
125+ }
126+ sortActivatedRouteSnapshots ( mergedChildren ) ;
127+ return mergedChildren ;
106128 }
107129
108130 processSegment (
@@ -129,9 +151,7 @@ export class Recognizer {
129151 processSegmentAgainstRoute (
130152 route : Route , rawSegment : UrlSegmentGroup , segments : UrlSegment [ ] ,
131153 outlet : string ) : TreeNode < ActivatedRouteSnapshot > [ ] | null {
132- if ( route . redirectTo ) return null ;
133-
134- if ( ( route . outlet || PRIMARY_OUTLET ) !== outlet ) return null ;
154+ if ( ! isImmediateMatch ( route , rawSegment , segments , outlet ) ) return null ;
135155
136156 let snapshot : ActivatedRouteSnapshot ;
137157 let consumedSegments : UrlSegment [ ] = [ ] ;
@@ -141,8 +161,9 @@ export class Recognizer {
141161 const params = segments . length > 0 ? last ( segments ) ! . parameters : { } ;
142162 snapshot = new ActivatedRouteSnapshot (
143163 segments , params , Object . freeze ( { ...this . urlTree . queryParams } ) , this . urlTree . fragment ! ,
144- getData ( route ) , outlet , route . component ! , route , getSourceSegmentGroup ( rawSegment ) ,
145- getPathIndexShift ( rawSegment ) + segments . length , getResolve ( route ) ) ;
164+ getData ( route ) , getOutlet ( route ) , route . component ! , route ,
165+ getSourceSegmentGroup ( rawSegment ) , getPathIndexShift ( rawSegment ) + segments . length ,
166+ getResolve ( route ) ) ;
146167 } else {
147168 const result : MatchResult | null = match ( rawSegment , route , segments ) ;
148169 if ( result === null ) {
@@ -153,7 +174,7 @@ export class Recognizer {
153174
154175 snapshot = new ActivatedRouteSnapshot (
155176 consumedSegments , result . parameters , Object . freeze ( { ...this . urlTree . queryParams } ) ,
156- this . urlTree . fragment ! , getData ( route ) , outlet , route . component ! , route ,
177+ this . urlTree . fragment ! , getData ( route ) , getOutlet ( route ) , route . component ! , route ,
157178 getSourceSegmentGroup ( rawSegment ) ,
158179 getPathIndexShift ( rawSegment ) + consumedSegments . length , getResolve ( route ) ) ;
159180 }
@@ -175,7 +196,17 @@ export class Recognizer {
175196 return [ new TreeNode < ActivatedRouteSnapshot > ( snapshot , [ ] ) ] ;
176197 }
177198
178- const children = this . processSegment ( childConfig , segmentGroup , slicedSegments , PRIMARY_OUTLET ) ;
199+ const matchedOnOutlet = getOutlet ( route ) === outlet ;
200+ // If we matched a config due to empty path match on a different outlet, we need to continue
201+ // passing the current outlet for the segment rather than switch to PRIMARY.
202+ // Note that we switch to primary when we have a match because outlet configs look like this:
203+ // {path: 'a', outlet: 'a', children: [
204+ // {path: 'b', component: B},
205+ // {path: 'c', component: C},
206+ // ]}
207+ // Notice that the children of the named outlet are configured with the primary outlet
208+ const children = this . processSegment (
209+ childConfig , segmentGroup , slicedSegments , matchedOnOutlet ? PRIMARY_OUTLET : outlet ) ;
179210 if ( children === null ) {
180211 return null ;
181212 }
@@ -234,6 +265,37 @@ function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment
234265 return { consumedSegments : res . consumed , lastChild : res . consumed . length , parameters} ;
235266}
236267
268+ /**
269+ * Finds `TreeNode`s with matching empty path route configs and merges them into `TreeNode` with the
270+ * children from each duplicate. This is necessary because different outlets can match a single
271+ * empty path route config and the results need to then be merged.
272+ */
273+ function mergeEmptyPathMatches ( nodes : Array < TreeNode < ActivatedRouteSnapshot > > ) :
274+ Array < TreeNode < ActivatedRouteSnapshot > > {
275+ const result : Array < TreeNode < ActivatedRouteSnapshot > > = [ ] ;
276+
277+ function hasEmptyConfig ( node : TreeNode < ActivatedRouteSnapshot > ) {
278+ const config = node . value . routeConfig ;
279+ return config && config . path === '' && config . redirectTo === undefined ;
280+ }
281+
282+ for ( const node of nodes ) {
283+ if ( ! hasEmptyConfig ( node ) ) {
284+ result . push ( node ) ;
285+ continue ;
286+ }
287+
288+ const duplicateEmptyPathNode =
289+ result . find ( resultNode => node . value . routeConfig === resultNode . value . routeConfig ) ;
290+ if ( duplicateEmptyPathNode !== undefined ) {
291+ duplicateEmptyPathNode . children . push ( ...node . children ) ;
292+ } else {
293+ result . push ( node ) ;
294+ }
295+ }
296+ return result ;
297+ }
298+
237299function checkOutletNameUniqueness ( nodes : TreeNode < ActivatedRouteSnapshot > [ ] ) : void {
238300 const names : { [ k : string ] : ActivatedRouteSnapshot } = { } ;
239301 nodes . forEach ( n => {
@@ -364,3 +426,35 @@ function getData(route: Route): Data {
364426function getResolve ( route : Route ) : ResolveData {
365427 return route . resolve || { } ;
366428}
429+
430+ /**
431+ * Determines if `route` is a path match for the `rawSegment`, `segments`, and `outlet` without
432+ * verifying that its children are a full match for the remainder of the `rawSegment` children as
433+ * well.
434+ */
435+ function isImmediateMatch (
436+ route : Route , rawSegment : UrlSegmentGroup , segments : UrlSegment [ ] , outlet : string ) : boolean {
437+ if ( route . redirectTo ) {
438+ return false ;
439+ }
440+ // We allow matches to empty paths when the outlets differ so we can match a url like `/(b:b)` to
441+ // a config like
442+ // * `{path: '', children: [{path: 'b', outlet: 'b'}]}`
443+ // or even
444+ // * `{path: '', outlet: 'a', children: [{path: 'b', outlet: 'b'}]`
445+ //
446+ // The exception here is when the segment outlet is for the primary outlet. This would
447+ // result in a match inside the named outlet because all children there are written as primary
448+ // outlets. So we need to prevent child named outlet matches in a url like `/b` in a config like
449+ // * `{path: '', outlet: 'x' children: [{path: 'b'}]}`
450+ // This should only match if the url is `/(x:b)`.
451+ if ( getOutlet ( route ) !== outlet &&
452+ ( outlet === PRIMARY_OUTLET || ! emptyPathMatch ( rawSegment , segments , route ) ) ) {
453+ return false ;
454+ }
455+ if ( route . path === '**' ) {
456+ return true ;
457+ } else {
458+ return match ( rawSegment , route , segments ) !== null ;
459+ }
460+ }
0 commit comments