How Angular resolves route-definitions (paths)


Let's first define a few basic things. We'll use this URL as an example: /users/james/articles?from=134#section
.
It may be obvious but let's first point out that query parameters (
?from=134
) and fragments (#section
) do not play any role in path matching. Only the base url (/users/james/articles
) matters.Angular splits URLs into segments. The segments of
/users/james/articles
are, of course,users
,james
andarticles
.The router configuration is a tree structure with a single root node. Each
Route
object is a node, which may havechildren
nodes, which may in turn have otherchildren
or be leaf nodes.
The goal of the router is to find a router configuration branch, starting at the root node, which would match exactly all (!!!) segments of the URL. This is crucial! If Angular does not find a route configuration branch which could match the whole URL - no more and no less - it will not render anything.
E.g. if your target URL is /a/b/c
but the router is only able to match either /a/b
or /a/b/c/d
, then there is no match and the application will not render anything.
Finally, routes with redirectTo
behave slightly differently than regular routes, and it seems to me that they would be the only place where anyone would really ever want to use pathMatch: full
. But we will get to this later.
Default (prefix
) path matching
The reasoning behind the name prefix
is that such a route configuration will check if the configured path
is a prefix of the remaining URL segments. However, the router is only able to match full segments, which makes this naming slightly confusing.
Anyway, let's say this is our root-level router configuration:
const routes: Routes = [
{
path: 'products',
children: [
{
path: ':productID',
component: ProductComponent,
},
],
},
{
path: ':other',
children: [
{
path: 'tricks',
component: TricksComponent,
},
],
},
{
path: 'user',
component: UsersonComponent,
},
{
path: 'users',
children: [
{
path: 'permissions',
component: UsersPermissionsComponent,
},
{
path: ':userID',
children: [
{
path: 'comments',
component: UserCommentsComponent,
},
{
path: 'articles',
component: UserArticlesComponent,
},
],
},
],
},
];
Note that every single Route
object here uses the default matching strategy, which is prefix
. This strategy means that the router iterates over the whole configuration tree and tries to match it against the target URL segment by segment until the URL is fully matched. Here's how it would be done for this example:
Iterate over the root array looking for an exact match for the first URL segment -
users
.'products' !== 'users'
, so skip that branch. Note that we are using an equality check rather than a.startsWith()
or.includes()
- only full segment matches count!:other
matches any value, so it's a match. However, the target URL is not yet fully matched (we still need to matchjames
andarticles
), thus the router looks for children.
- The only child of
:other
istricks
, which is!== 'james'
, hence not a match.
Angular then retraces back to the root array and continues from there.
'user' !== 'users
, skip branch.'users' === 'users
- the segment matches. However, this is not a full match yet, thus we need to look for children (same as in step 3).
'permissions' !== 'james'
, skip it.:userID
matches anything, thus we have a match for thejames
segment. However this is still not a full match, thus we need to look for a child which would matcharticles
.- We can see that
:userID
has a child routearticles
, which gives us a full match! Thus the application rendersUserArticlesComponent
.
- We can see that
Full URL (full
) matching
Example 1
Imagine now that the users
route configuration object looked like this:
{
path: 'users',
component: UsersComponent,
pathMatch: 'full',
children: [
{
path: 'permissions',
component: UsersPermissionsComponent,
},
{
path: ':userID',
component: UserComponent,
children: [
{
path: 'comments',
component: UserCommentsComponent,
},
{
path: 'articles',
component: UserArticlesComponent,
},
],
},
],
}
Note the usage of pathMatch: full
. If this were the case, steps 1-5 would be the same, however step 6 would be different:
'users' !== 'users/james/articles
- the segment does not match because the path configurationusers
withpathMatch: full
does not match the full URL, which isusers/james/articles
.Since there is no match, we are skipping this branch.
At this point we reached the end of the router configuration without having found a match. The application renders nothing.
Example 2
What if we had this instead:
{
path: 'users/:userID',
component: UsersComponent,
pathMatch: 'full',
children: [
{
path: 'comments',
component: UserCommentsComponent,
},
{
path: 'articles',
component: UserArticlesComponent,
},
],
}
users/:userID
with pathMatch: full
matches only users/james
thus it's a no-match once again, and the application renders nothing.
Example 3
Let's consider this:
{
path: 'users',
children: [
{
path: 'permissions',
component: UsersPermissionsComponent,
},
{
path: ':userID',
component: UserComponent,
pathMatch: 'full',
children: [
{
path: 'comments',
component: UserCommentsComponent,
},
{
path: 'articles',
component: UserArticlesComponent,
},
],
},
],
}
In this case:
'users' === 'users
- the segment matches, butjames/articles
still remains unmatched. Let's look for children.
'permissions' !== 'james'
- skip.:userID'
can only match a single segment, which would bejames
. However, it's apathMatch: full
route, and it must matchjames/articles
(the whole remaining URL). It's not able to do that and thus it's not a match (so we skip this branch)!
- Again, we failed to find any match for the URL and the application renders nothing.
As you may have noticed, a pathMatch: full
configuration is basically saying this:
Ignore my children and only match me. If I am not able to match all of the remaining URL segments myself, then move on.
Redirects
Any Route
which has defined a redirectTo
will be matched against the target URL according to the same principles. The only difference here is that the redirect is applied as soon as a segment matches. This means that if a redirecting route is using the default prefix
strategy, a partial match is enough to cause a redirect. Here's a good example:
const routes: Routes = [
{
path: 'not-found',
component: NotFoundComponent,
},
{
path: 'users',
redirectTo: 'not-found',
},
{
path: 'users/:userID',
children: [
{
path: 'comments',
component: UserCommentsComponent,
},
{
path: 'articles',
component: UserArticlesComponent,
},
],
},
];
For our initial URL (/users/james/articles
), here's what would happen:
'not-found' !== 'users'
- skip it.'users' === 'users'
- we have a match.This match has a
redirectTo: 'not-found'
, which is applied immediately.The target URL changes to
not-found
.The router begins matching again and finds a match for
not-found
right away. The application rendersNotFoundComponent
.
Now consider what would happen if the users
route also had pathMatch: full
:
const routes: Routes = [
{
path: 'not-found',
component: NotFoundComponent,
},
{
path: 'users',
pathMatch: 'full',
redirectTo: 'not-found',
},
{
path: 'users/:userID',
children: [
{
path: 'comments',
component: UserCommentsComponent,
},
{
path: 'articles',
component: UserArticlesComponent,
},
],
},
];
'not-found' !== 'users'
- skip it.users
would match the first segment of the URL, but the route configuration requires afull
match, thus skip it.'users/:userID'
matchesusers/james
.articles
is still not matched but this route has children.
- We find a match for
articles
in the children. The whole URL is now matched and the application rendersUserArticlesComponent
.
Empty path (path: ''
)
The empty path is a bit of a special case because it can match any segment without "consuming" it (so its children would have to match that segment again). Consider this example:
const routes: Routes = [
{
path: '',
children: [
{
path: 'users',
component: BadUsersComponent,
}
]
},
{
path: 'users',
component: GoodUsersComponent,
},
];
Let's say we are trying to access /users
:
path: ''
will always match, thus the route matches. However, the whole URL has not been matched - we still need to matchusers
!We can see that there is a child
users
, which matches the remaining (and only!) segment and we have a full match. The application rendersBadUsersComponent
.
Subscribe to my newsletter
Read articles from Oliver Waterkamp directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
