[Aurelia 2] Help Needed: finalInstruction.children is empty at au:router:navigation-end

I’ve been working through migrating my apps to Aurelia 2, and I’ve hit a just another roadblock that’s left me pretty stumped (and admittedly frustrated) after weeks of migrating. At the au:router:navigation-end event, I’m finding that the finalInstruction.children array is empty. From what I can tell, this is the only reliable way to access the current route details via finalInstruction.children[0].recognizedRoute.route.endpoint.route.handler, but without the children populated, I’m stuck.

I’ve been bouncing between dependency injection quirks and router issues for a while now, and I’d really appreciate any insights.

Update: Looks like something is wrong with my route config, and I need to take a closer look at the redirects.

Greatly appreciated if you could share the cause and the solution if possible! It might help future development :slight_smile:

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 :slight_smile: 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.

4 Likes

Thanks anyway! Appreciated!