Lets say my url is: http://localhost:4200/user_id/home. Here is my button code:
<ion-button [routerLink]="['some_user_id', 'payments']" routerLinkActive="selected">
<ion-label class="label">Payments</ion-label>
</ion-button>
Because I was getting Error: Cannot match any routes.
I started to investigate the problem and I come to, that routerLink is generating such DOM element:
<a href="user_id/home/(some_user_id/payments)" class="button-native" part="native">
When (in the same component) I use router to navigate, like:
this.router.navigate('some_user_id', 'payments'])
all works.
What is the problem that generated href isn't just <a href="some_user_id/payments" class="button-native" part="native"> as allways
?
It's because routerLink
is a directive and it does a few other things behind the scenes.
Let's see what happens when you click on an element which has the RouterLink
directive:
@Directive({selector: ':not(a):not(area)[routerLink]'})
export class RouterLink {
/* ... */
@HostListener('click')
onClick(): boolean {
const extras = {
skipLocationChange: attrBoolValue(this.skipLocationChange),
replaceUrl: attrBoolValue(this.replaceUrl),
state: this.state,
};
this.router.navigateByUrl(this.urlTree, extras);
return true;
}
get urlTree(): UrlTree {
return this.router.createUrlTree(this.commands, {
relativeTo: this.route, // !
queryParams: this.queryParams,
fragment: this.fragment,
preserveQueryParams: attrBoolValue(this.preserve),
queryParamsHandling: this.queryParamsHandling,
preserveFragment: attrBoolValue(this.preserveFragment),
});
}
/* ... */
}
Keep an eye on relativeTo: this.route
, where this.route
points to the current ActivatedRoute
(e.g the one associated with /home
).
What Router.createUrlTree
does is to apply a set of commands
to the current URL tree, which will result in a new URL tree. In your case, the commands
are ['some_user_id', 'payments']
.
createUrlTree(commands: any[], navigationExtras: NavigationExtras = {}): UrlTree {
const {
relativeTo,
queryParams,
fragment,
preserveQueryParams,
queryParamsHandling,
preserveFragment
} = navigationExtras;
/* .... */
const a = relativeTo || this.routerState.root;
const f = preserveFragment ? this.currentUrlTree.fragment : fragment;
let q: Params|null = null;
/* ... resolving query params based on the `queryParamsHandling` strategy */
return createUrlTree(a, this.currentUrlTree, commands, q!, f!);
}
createUrlTree
is where the magic happens:
export function createUrlTree(
route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params,
fragment: string): UrlTree {
// `route` - that one which corresponds to `/home`
// `commends` - `['some_user_id', 'payments']`
// `urlTree` - a tree of UrlSegmentGroups, we'll have a closer look a bit later
if (commands.length === 0) { /* Not our case */ }
/*
a command might also be one of these objects:
* { outlets: { outletName: path } }
* { k1: v1, k2: v2 } - segment parameters
* { segmentPath: path }
but in this case, it will simply be a Navigation object {
isAbsolute: false,
numberOfDoubleDots: 0,
commands: ['some_user_id', 'payments']
}
*/
const nav = computeNavigation(commands);
if (nav.toRoot()) {
/* Not our case; */
/* It would've been if: this.isAbsolute && this.commands.length === 1 && this.commands[0] == '/' */
}
/*
We'd get a new `Position` object: `return new Position(g, false, ci - dd);`
where `dd` - number of double dots = 0 and `ci` - current index = 1
why is it 1? - https://github.com/angular/angular/blob/master/packages/router/src/create_url_tree.ts#L160
*/
const startingPosition = findStartingPosition(nav, urlTree, route);
const segmentGroup = startingPosition.processChildren ?
updateSegmentGroupChildren(
startingPosition.segmentGroup, startingPosition.index, nav.commands) :
updateSegmentGroup(startingPosition.segmentGroup, startingPosition.index, nav.commands);
return tree(startingPosition.segmentGroup, segmentGroup, urlTree, queryParams, fragment);
}
segmentGroup
would be the result of updateSegmentGroup
. It will eventually reach createNewSegmentGroup
:
function createNewSegmentGroup(
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
// Everything before the `startIndex`
const paths = segmentGroup.segments.slice(0, startIndex);
let i = 0;
while (i < commands.length) {
if (typeof commands[i] === 'object' && commands[i].outlets !== undefined) {
/* Not our case */
}
// if we start with an object literal, we need to reuse the path part from the segment
// That's why the `modifier` is 1 if there are no parameters: https://github.com/angular/angular/blob/master/packages/router/src/create_url_tree.ts#L160
if (i === 0 && isMatrixParams(commands[0])) {
const p = segmentGroup.segments[startIndex];
paths.push(new UrlSegment(p.path, commands[0]));
i++;
continue;
}
const curr = getPath(commands[i]);
const next = (i < commands.length - 1) ? commands[i + 1] : null;
if (curr && next && isMatrixParams(next)) {
paths.push(new UrlSegment(curr, stringify(next)));
i += 2;
} else {
// Adding the commands(`['some_user_id', 'payments']`) the the previous segments
// Which explains why you're getting the current behavior
paths.push(new UrlSegment(curr, {}));
i++;
}
}
return new UrlSegmentGroup(paths, {});
}
Note: this walk-through is based on this ng-run demo.
An URL can have this structure: segment?queryParams#fragment
.
An UrlSegmentGroup
can have an array of UrlSegments
and an object of child UrlSegmentGroup
s:
export class UrlSegmentGroup {
/* ... */
parent: UrlSegmentGroup|null = null;
constructor(
public segments: UrlSegment[],
public children: {[key: string]: UrlSegmentGroup}) {
forEach(children, (v: any, k: any) => v.parent = this);
}
/* ... */
}
For example, we might have a more complex URL, such as foo/123/(a//named:b)
. The resulted UrlSegmentGroup
will be this:
{
segments: [], // The root UrlSegmentGroup never has any segments
children: {
primary: {
segments: [{ path: 'foo', parameters: {} }, { path: '123', parameters: {} }],
children: {
primary: { segments: [{ path: 'a', parameters: {} }], children: {} },
named: { segments: [{ path: 'b', parameters: {} }], children: {} },
},
},
},
}
which would match a route configuration like this:
{
{
path: 'foo/:id',
loadChildren: () => import('./foo/foo.module').then(m => m.FooModule)
},
// foo.module.ts
{
path: 'a',
component: AComponent,
},
{
path: 'b',
component: BComponent,
outlet: 'named',
},
}
You can experiment with this example in this StackBlitz.
As you can see, UrlSegmentGroup
's children are delimited by ()
. The names of these children are the router outlet.
In /(a//named:b)
, because it uses a /
before (
, a
will be segment of the primary outlet. //
is the separator for router outlets. Finally, named:b
follows this structure: outletName:segmentPath
.
Another thing that should be mentioned is the UrlSegment
's parameters
property. Besides positional parameters(e.g foo/:a/:b
), segments can have parameters declared like this: segment/path;k1=v1;k2=v2
;
So, an UrlTree
has 3 important properties: the root
UrlSegmentGroup
, the queryParams
object and the fragment
of the issued URL.
this.router.navigate('some_user_id', 'payments'])
works because Router.navigate
will eventually call Router.createUrlTree
:
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> {
validateCommands(commands);
return this.navigateByUrl(this.createUrlTree(commands, extras), extras);
}
Then, const a = relativeTo || this.routerState.root;
will be reached inside Router.createUrlTree
and since there is no relativeTo
(as opposed to RouterLink
), it will be relative to the root ActivatedRoute
.
You can get the same behavior with routerLink
, by adding /
at the beginning of the first command: [routerLink]="['/some_user_id', 'payments']"