angularangular-router

How to implement nested routing in Angular (16+) using stand-alone components


I would like to achieve nested routing using two separate route configs and two router-outlets. However, I run into the problem that I can't provide Routes in my stand-alone component that is loaded in a router-outlet itself.

What I would like to achieve without (if possible) adding all my router paths to my app routing: Structure I would like to achieve

I tried providing routes to my component (Two Component, see previous image) that is loaded inside a router-outlet itself:

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule, Routes, provideRouter } from '@angular/router';

export const twoRoutes: Routes = [
    {
        path: 'three',
        loadComponent: () => import('./three.component').then(a => a.ThreeComponent),
    },
    {
        path: 'four',
        loadComponent: () => import('./four.component').then(a => a.FourComponent),
    },
];

@Component({
    selector: 'app-two',
    standalone: true,
    imports: [CommonModule, RouterModule],
    providers: [provideRouter(twoRoutes)],
    template: `
        <div>Hello Component TWO</div>
        <router-outlet></router-outlet>
    `,
    styles: ``,
})
export class TwoComponent {}

Error: using provideRouter in a stand-alone component

I expected there to be a way to define my nested routes in my stand-alone component instead of having to add all of these router paths to my app routes and children.

UPDATE: I figured out the following works using loadChildren, yet I personally do not like this since this requires me to update the app Routes, making it aware of nested routes:

import { Routes } from '@angular/router';

export const routes: Routes = [
    {
        path: 'one',
        loadComponent: () => import('./one.component').then(a => a.OneComponent),
    },
    {
        path: 'two',
        loadComponent: () => import('./two.component').then(a => a.TwoComponent),
        loadChildren: () => import('./two.component').then(t => t.twoRoutes),
    },
];


Solution

  • As I understood you would like to have like decoupled routing definitions. So that your feature routes do not need to be provided within the root route object.

    loadChildren is indeed the correct choice here. Routing IS a way of coupling dependencies, but do it loosely instead of strong coupling like using your components within other components (the component tree is strong coupling).

    The following pattern is broadly used as I have seen it quite often.

    Let's assume you have an application which consists of features

    Now what I understood is that you do not want the app routes to be in any way dependent on Car Management Feature or Employee Management Feature. Where is the point? Your application consists of both features, so it should know the features. But what is possible, to let the app routes only "see" the features route object (imports omitted):

    export const employeeManagementRoutes: Routes = [
      {
        path: '',
        // component: TwoComponent, // do not define a component here, instead just define child routes
        children: [
          {
            path: 'vacation-calendar',
            component: VacationCalendarComponent,
          },
          {
            path: 'employees',
            // component: EmployeeListComponent,
            children: async () => (await loadChildren('./some-other-subfeature')).subFeatureRoutes
          },
          {
             path: '',
             pathMatch: 'full',
             component: EmployeeManagementRootComponent
          }
          {
            path: '**', // route every undefined route to the root of this feature
            redirectTo: ''
          }
        ],
      },
    ];
    

    And therefore in your app-routes.ts

    export const routes: Routes = [
        {
            path: 'one',
            loadComponent: () => import('./one.component').then(a => a.OneComponent),
        },
        {
            path: 'employee-management',
            // do not use loadComponent here as you do not want to leak the internals of your feature into your app
            loadChildren: () => import('./employee-management/routes').then(feature => feature.employeeManagementRoutes),
        },
    ];
    

    What you would like to do is turning this around and having the feature know how to be routed to. This does not make sense as there is only ONE Router for the entire application which therefore needs to know the routes on its own. The router is used to provide a way of loosely coupling components and compose an application.

    Angular changed the way of routing to be an array of routes instead of a clunky router module. Please do not use modules anymore (i hope they will deprecate modules in the future as they do not serve purpose anymore). With this new approach the only dependency that an application has to it's features is this routes array.

    Defining the routes as a static field of a feature component destroys this whole loosely coupling idea as you will have a dependency to the component class then.

    The whole routing tree must be either known on bootstrapping or lazy loaded via loadChildren.

    What comes close to what you would like to achieve might be microfrontends with module federation:

    https://www.angulararchitects.io/blog/the-microfrontend-revolution-part-2-module-federation-with-angular/

    But even here does to composing application know it's dependencies.

    Conclusion:

    With the router you are able to decouple features from each other. With angulars provideRouter() function and loadChildren loading another route array it is even possible to decouple your app from it's features internals. No need to load a component, just load the feature routes and let the feature decide everything else.