2

I can't for the love of cookies figure out how to configure the most recent incarnation of the Angular 2 router (the one from the final release). I have a rather large Angular 2 RC5 app up and running, but need to migrate to the final release now before I can move on.

In this app I have a lot of nested components that, according to Google's docs, should be organised as features in module. The typical URL in my app looks like so

user/34/section/95/client/13/<feature>

but there are also a few others, such as

user/34/settings

so in this case, settings would be a feature module.

I've read the docs, which are rather long-winded and are in no hurry to cut to the chase, only to find out that this case isn't covered at all. I can't believe that this scenario isn't going to work, it would seem to be a huge oversight so I suppose the problem is that I haven't quite grasped how this router actually works - and I have a feeling that I'm not the only one.

I've created this plunker to illustrate my point.

In this plunker I got 2 levels of nesting

app -> benutzer (user) -> klient (client)

just enough to make the issue appear. The expected outcome is that I can navigate to

benutzer/78/klient/56

i.e. the router actually finds that route, which at the moment it doesn't.

Before I moved the code to plunker and added dashboard.component (because I can't easily modify the URL in plunker, so I have to resort to routerLink) I could actually navigate to

klient/56

which shouldn't be possible. It seems that each route defined in another module is added to the root instead of building on top of each other.

Any help with this is greatly appreciated.

Thorsten Westheider
  • 9,252
  • 12
  • 51
  • 92

2 Answers2

9

Look at all your routes.

appRoutes = [      // AppModule
  { path: '', redirectTo: 'dashaboard' },
  { path: 'dashboard', component: DashboardComponent
];
benutzerRoutes = [ // BenutzerModule
  { path: 'benutzer/:id', component BenutzerComponent }
];
klientRoutes = [   // KlinetModule
  { path: 'klient/:id', component: KlientComponent }
]

The way you import your modules doesn't have any effect on how the routes are constructed. How they are constructed are simply based on how we construct the. What you are expecting with this

AppModule
   imports -> BenutzerModule
                   imports -> KlinetModule

leading to the following routes

dashboard/benutzer/:id/klient/:id

is just not the case. Each one of those route arrays are simply added to the root. That's why you can access klient/:id and not dashboard/benutzer/:id.

I've read the complete documentation for routing a few times, and there aren't any example of nested routes in different modules. All the examples had modules that were loaded from the root route, as your example does, or having nested routes being part of the same route configuration. So since there are no example I guess we need to just work with what we know and decide for ourselves what's the best way.

There are a couple ways I can think of. The first, most obvious but IMO more intrusive than the next option, is to just add the full routes to the paths

appRoutes = [      // AppModule
  { path: '', redirectTo: 'dashaboard' },
  { path: 'dashboard', component: DashboardComponent
];
benutzerRoutes = [ // BenutzerModule
  { path: 'dashboard/benutzer/:id', component BenutzerComponent }
];
klientRoutes = [   // KlinetModule
  { path: 'dashboard/benutzer/:id/klient/:id', component: KlientComponent }
]

I say this option is more intrusive, as it forces the children to know about the parent. In Angular 2 architecture that is counter to how we structure our components. Parents should know about children, but not necessarily vice versa.

The other option I can think of is to use loadChildren to load the child modules lazily. I say "lazily" because I can't figure out how to do this eagerly, and not sure if it is even possible to do so. To load the children lazily, we can do

export const appRoutes: Routes = [
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  {
    path: 'dashboard/benutzer',
    loadChildren: 'app/benutzer/benutzer.module#BenutzerModule'
  },
  { path: '**', component: NotFoundComponent }
];
export const benutzerRoutes: Routes = [
  { path: ':id', component: BenutzerComponent },
  {
    path: ':id/klient',
    loadChildren: 'app/klienten/klienten.module#KlientenModule'
  }
];
export const klientenRoutes: Routes = [
  { path: ':id', component: KlientComponent }
];

In this case we would remove the all the child module imports from their parent @NgModule. This allows for lazy loading of the module. If we leave then, then the module will be loaded eagerly on startup, but not have the desired effect (hence why I said I am not how to do this eagerly).

Also notice the loadChildren. In the above example I use app as the root. The only reason is that I tested in local environment. I am not a big fan of Plunker. For your Plunker though, you should use src as the root.

IMO, the lazy loading looks cleaner, as child doesn't know about parent, but this forces you to lazily load the modules, which may not be desired. In some cases though, it is desired, as it allows for a lighter initial load.

For more on lazy loading, see the docs routing section on Asynchronous Routing

jna
  • 160
  • 1
  • 8
Paul Samsotha
  • 188,774
  • 31
  • 430
  • 651
  • Indeed, there's lots of documentation and this real world scenario isn't dealt with at all. Actually, they hint at nested modules/routes but they never come up with an example, so my guess is they simply skipped this feature for final. – Thorsten Westheider Sep 22 '16 at 05:54
  • Marked as answer because of the alternative solution using lazy loading, which is what I settled for in the end. – Thorsten Westheider Sep 22 '16 at 15:09
  • 1
    I picked the second solution too, but it's freak me out, that such a simple thing have not been implemented. – Mikki Nov 27 '16 at 18:31
1

Found a possible solution, copying my answer from https://stackoverflow.com/a/39629949/370935

I got this to work as well and unless you actually need to render all parent components in the hierarchy I think my solution is far more elegant.

The key to understanding my approach is that all routes, no matter how deeply nested in modules are added to the root module. Quick example, let's say we have a DepartmentModule and an EmployeeModule which we'd like to navigate to using this URL

/department/1/employee/2

at which point we'd see employee 2's details. Configuring routes for department in department.routing.ts and employee in employee.routing.ts will not work the way we intended and you'll notice that you can navigate to

/employee/2

from the root component, while

/department/1/employee/2

will crash (route not found). A typical route configuration in this scenario would look like this:

export const departmentRoutes: Routes = [
    { path: 'department', component: DepartmentComponent, children: [
        { path: '', component: DepartmentsComponent },
        { path: ':id', component: DepartmentDetailsComponent }
    ]}
];

export const employeeRoutes: Routes = [
    { path: 'employee', component: EmployeeComponent, children: [
        { path: '', component: EmployeesComponent },
        { path: ':id', component: EmployeeDetailsComponent }
    ]}
];

and EmployeeModule would be imported by DepartmentModule. Now, like I said, that doesn't work unfortunately.

However, with just a single change it will:

export const employeeRoutes: Routes = [
    { path: 'department/:id/employee', component: EmployeeComponent, children: [
        { path: '', component: EmployeesComponent },
        { path: ':id', component: EmployeeDetailsComponent }
    ]}
];

The catch is, that DepartmentModule is not taking an active part anymore as soon you navigate to an employee URL, but you still can access every parameter from the ActivatedRoute:

export class EmployeeDetailsComponent {
    departmentId: number;
    employeeId: number;
    constructor(route: ActivatedRoute) {
        route.parent.params.subscribe(params =>
            this.departmentId= +params['id'])
        route.params.subscribe(params => 
            this.employeeId= +params['id']);
    }
}

I wonder if this is supposed to be the official approach, but for now this works for me until the next breaking change from the Angular 2 team .

Community
  • 1
  • 1
Thorsten Westheider
  • 9,252
  • 12
  • 51
  • 92
  • I was just testing and writing up [an answer](http://stackoverflow.com/a/39630242/2587435) when you posted yours :-) The first part of my answer provides a solution pretty much the same as yours, but offers a second solution of lazy loading. Check it out. – Paul Samsotha Sep 22 '16 at 04:26