Well, it’s probably too specific for my app, but I’ll share it anyway. So, in Aurelia 1 I had the following route config, simplified:
public configureRouter(config: RouterConfiguration, router: Router): void {
const step: PipelineStep = {
run: async (instruction: NavigationInstruction, next: Next): Promise<unknown> => {
this.authorized = await this.session.getAuthorizationSatus();
const result =
(await this.checkAuthorization(instruction, next)) ||
(await this.checkOrigin(instruction, next));
if (result) {
return result;
}
return next();
},
};
config.addAuthorizeStep(step);
config.options.pushState = true;
config.options.hashChange = false;
const routes: RouteConfig[] = [
{
route: ["", "/"],
moduleId: PLATFORM.moduleName("./pages/login/login"),
name: "login",
nav: false,
auth: false,
title: "",
activationStrategy: activationStrategy.replace,
},
{
route: ["tickets"],
moduleId: PLATFORM.moduleName("./pages/reports/reports"),
name: "reports",
nav: true,
auth: true,
title: "mmenu.reports",
activationStrategy: activationStrategy.replace,
},
{
route: "login",
moduleId: PLATFORM.moduleName("./pages/login/login"),
name: "login",
nav: false,
auth: false,
title: "auth.login",
},
...
];
config.map(routes);
router.ensureConfigured().then(() => {
this.navTree = this.createNavTree(router.navigation);
});
}
private async checkAuthorization(
instruction: NavigationInstruction,
next: Next
): Promise<unknown> {
if (instruction.getAllInstructions().some((i) => i.config.auth)) {
if (!this.authorized) {
const currentUrl =
instruction.fragment +
(instruction.queryString ? `?${instruction.queryString}` : "");
if (currentUrl && currentUrl !== "/") {
setOrigin(currentUrl);
}
return next.cancel(new Redirect("login"));
}
} else if (
this.authorized &&
instruction.getAllInstructions().some((i) => i.fragment === "login")
) {
return next.cancel(new Redirect("/tickets"));
} else if (
this.authorized &&
instruction.getAllInstructions().some((i) => i.fragment === "/")
) {
return next.cancel(new Redirect("tickets"));
}
}
private async checkOrigin(instruction: NavigationInstruction, next: Next): Promise<unknown> {
const origin = this.state.origin;
if (origin && instruction.fragment !== "/login") {
await clearOrigin();
return next.cancel(new Redirect(origin));
}
}
As you can see, the default route was set to login page and also authorize pipeline step was used to handle auth and navigate out of the login page when necessary. When it was converted to Aurelia 2, the auth step became the auth lifecycle hook, but the logic remained the same. Also, at this point I believe I was unsuccessful in creating the multiple routes with the same component, so the route config was also changed:
routes: [
{
path: "", // Empty route
redirectTo: "login", // Default to "login" if unauthorized; AuthorizeHook will override if authorized
},
{
path: "login",
component: Login,
id: "login",
nav: false,
title: "login",
data: { auth: false },
},
{
path: "tickets/:id",
component: Reports,
id: "report",
nav: false,
title: "report.ticket",
data: { auth: true },
transitionPlan: RouterTransitionPlan.Replace,
},
...
],
This resulted in an infinite loop of triggering “au:router:navigation-start” navigation events. In an attempt to investigate the problem, I first commented out the suspicious redirect to login from the empty route:
// {
// path: "",
// redirectTo: "login",
// },
When that didn’t help after some debugging, I suddenly realized that I was still relying on the fragment
property in my checkAuthorization()
function, and that I had read somewhere that the concept of “fragment” had changed between the two versions, and that relying on it in the same way as in Aurelia 1 was probably the reason for my navigation loop.
In Aurelia 1 the router’s fragment property in navigation instructions represented the full path being navigated to, relative to the base URL. Aurelia 2’s router-lite fragment property has a more specific meaning: it represents the hash fragment of the URL (e.g., in /tickets#section1, fragment is “section1”), not the full path. The full route path is now accessed via RouteNode.path
, and the matched route’s identifier is typically RouteNode.instruction.recognizedRoute.route.endpoint.route.handler.id
(!).
My AuthorizeHook uses this.fragment
to decide redirects:
if (this.authorized) {
if (this.fragment === "login" || this.fragment === "") {
return "tickets";
}
}
If fragment is “”, as in my case, because I’m not using hash (useUrlFragmentHash: false
), this condition triggers a redirect to “tickets” even when the current path is already “tickets”. This creates a loop.
I fixed that, and even though I feel like it doesn’t pass my own quality control, it works:
private checkAuthorization(next: RouteNode): string | null {
const routeId =
(next.instruction?.recognizedRoute?.route?.endpoint?.route?.handler as any)?.id ||
next.path;
// For auth-required routes, redirect to login if not authenticated
if (next.data?.auth === true && !this.authorized) {
const query = next.queryParams;
const currentUrl =
next.path + (Object.keys(query).length ? `?${new URLSearchParams(query)}` : "");
if (currentUrl && currentUrl !== "/" && currentUrl !== "login") {
this.actions.setOrigin(currentUrl);
}
return LOGIN_ROUTE_ID;
}
// For authenticated users, redirect from login or root to tickets
if (this.authorized && routeId !== TICKETS_ROUTE_ID) {
if (routeId === LOGIN_ROUTE_ID || routeId === "" || routeId === "/") {
return TICKETS_ROUTE_ID;
}
}
return null;
}
And this is where I got the empty children array from the original post that caused the problem of not being able to get route details. And the reason was simple: I had no children populated because I had no default route! In all the attempts to fix the loop, I forgot to uncomment the default route redirect to the login page! This one:
{
path: "",
redirectTo: "login",
},
When I put it back, the routing now works, until the next issue
But now some custom elements are not rendering, but this has nothing to do with the routing.
As for router-lite, I found it complicated and confusing. There should be a better way to get current route details than going through a very deep structure of almost identical but different objects and guessing which one is which at what time. currentTr
.instruction
or.finalInstruction
? recognizedRoute
, route
, endpoint
, route
(again!), handler
? Some of this objects looks the same but parameters with the same names holds different entities, some types are missing, some are overlapping and confusing etc. There is a space for improvement.