69

I am in the process upgrading an application I'm working on to the latest Angular 2 release candidate. As part of this work I am attempting to use the NgModule spec and migrating all of the parts of my application to modules. For the most part, this has gone very well with the exception of an issue with routing.

"@angular/common": "2.0.0-rc.5",
"@angular/compiler": "2.0.0-rc.5",
"@angular/core": "2.0.0-rc.5",
"@angular/forms": "0.3.0",
"@angular/http": "2.0.0-rc.5",
"@angular/platform-browser": "2.0.0-rc.5",
"@angular/platform-browser-dynamic": "2.0.0-rc.5",
"@angular/router": "3.0.0-rc.1",

My app is built as a composition of modules, with several modules being glued together as children of a parent module. For example, I have an Admin Module that consists of a Notifications Module, a Users Module, and a Telphony Module (for example). The routes to these modules should look like...

/admin/notifications/my-notifications
/admin/users/new-user
/admin/telephony/whatever

In the earlier release of the router, this was easy to accomplish using "children"

export const AdminRoutes: RouterConfig = [
   {
      path: "Admin",
      component: AdminComponent,
      Children: [
         ...UserRoutes,
         ...TelephonyRoutes,
         ...NotificationRoutes
      ]
   }
]

In another file, as part of the sub-modules, I'd define the individual module routes as well i.e.

export const UserRoutes: RouterConfig = [
    {
       path: "users",
       component: userComponent,
       children: [
           {path: "new-user", component: newUserComponent}
       ]
    }

]

This all worked very well. In the process of upgrading to Modules, I moved everything into their own individual routing files instead so now these two look more like this

const AdminRoutes: Routes = [
    {path: "admin", component: AdminComponent}
] 

export const adminRouting = RouterModule.forChild(AdminRoutes)

and

const UserRoutes: Routes = [
       path: "users",
       component: userComponent,
       children: [
           {path: "new-user", component: newUserComponent}
       ]
] 

export const userRouting = RouterModule.forChild(UserRoutes)

With all of that in place, I have a UsersModule which imports the userRouting, and then an AdminModule that imports the adminRoutes and the UsersModule. My thought was that since UsersModule is a child of AdminModule, the routing would work the way it used to. Unfortunately, it doesn't so I end up with a users route that is just

/users/new-user 

instead of

/admin/users/new-user

Further, because of this, the new-user component isn't loaded into the router outlet of my admin component which throws off the styling and navigation of my application.

I can't for the life of me come up with how to reference the routes of my UserModule as children of my AdminModule. I've tried doing this the old way and get errors about the routes being in two Modules. Obviously since this is newly released, the documentation around some of these cases is a bit limited.

Any help anyone can provide would be greatly appreciated!

Don Vitali
  • 29
  • 5
John Ackerman
  • 1,021
  • 1
  • 8
  • 15
  • 1
    There is a `loadChildren` property on routes that allows to explicitly mention a module. The docs only speak about it in the context of lazy loading though ... – Marcus Riemer Aug 13 '16 at 08:20
  • I think this is the document that you should be referring to https://angular.io/guide/ngmodule#lazy-loading-modules-with-the-router – jitenagarwal19 Sep 03 '17 at 17:18
  • I found [this article](https://toddmotto.com/angular-component-router) by Todd Motto about the Angular Router very useful. `loadChildren` is also explained very well. – Hinrich Jan 24 '18 at 13:20

7 Answers7

56

Okay, after fiddling around with this for the better part of the weekend I got it running on my end. What worked for me in the end was to do the following:

  • Export all Routes for every module you want to route. Do not import any of the RouterModule.forChild() in the child modules.
  • Export every component that is visible from the childs route definitions in the childs module definition.
  • Import (meaning the Typescript import keyword) all child routes as usual and use the ... operator to incorporate these under the correct path. I couldn't get it to work with the child-module defining the path, but having it on the parent works fine (and is compatible to lazy loading).

In my case I had three levels in a hierarchy like this:

  • Root (/)
    • Editor (editor/:projectId)
      • Query (query/:queryId)
      • Page (page/:pageId)
    • Front (about)

The following definitions work for me for the /editor/:projectId/query/:queryId path:

// app.routes.ts
import {editorRoutes}                   from './editor/editor.routes'

// Relevant excerpt how to load those routes, notice that the "editor/:projectId"
// part is defined on the parent
{
    path: '',
    children: [
        {
            path: 'editor/:projectId',
            children: [...editorRoutes]
            //loadChildren: '/app/editor/editor.module'
        },
    ]
}

The editor routes look like this:

// app/editor/editor.routes.ts
import {queryEditorRoutes}              from './query/query-editor.routes'
import {pageEditorRoutes}               from './page/page-editor.routes'

{
    path: "", // Path is defined in parent
    component : EditorComponent,
    children : [
        {
            path: 'query',
            children: [...queryEditorRoutes]
            //loadChildren: '/app/editor/query/query-editor.module'
        },
        {
            path: 'page',
            children: [...pageEditorRoutes]
            //loadChildren: '/app/editor/page/page-editor.module'
        }
    ]
}

And the final part for the QueryEditor looks like this:

// app/editor/query/query-editor.routes.ts
{
    path: "",
    component : QueryEditorHostComponent,
    children : [
        { path: 'create', component : QueryCreateComponent },
        { path: ':queryId', component : QueryEditorComponent }
    ]
}

However, to make this work, the general Editor needs to import and export the QueryEditor and the QueryEditor needs to export QueryCreateComponent and QueryEditorComponent as these are visible with the import. Failing to do this will get you errors along the lines of Component XYZ is defined in multiple modules.

Notice that lazy loading also works fine with this setup, in that case the child-routes shouldn't be imported of course.

Marcus Riemer
  • 6,034
  • 7
  • 47
  • 66
  • 1
    This looks promising, and I appreciate the time you put into it. Can you clarify a little on your module definitions, and what exactly you're exporting from the routing files? Are you exporting the Routes[] or are you exporting the RouterModule.forChild() from the routing files? And where do you export/import those in the modules to get them visible? My attempts so far have resulted in an error about RouterOutlet being defined twice, and then an issue trying to export the Routes[] in the module as unexpect [object, Object] being exported. – John Ackerman Aug 15 '16 at 22:07
  • 3
    Oh, sorry ... I somehow read your comment and then didn't get around answering it. You can see the whole setup I ended up with at https://bitbucket.org/marcusriemer/esqulino/src/e1338eea74e42f0468f28554cd6a150f9295350a/client/app/?at=master – Marcus Riemer Aug 19 '16 at 18:35
  • I spent some time to get to this answer. Thank you for clarification and detailed answer. Any chance to get a link to angular documentation? – eagle.dan.1349 Oct 11 '16 at 10:09
  • I am not quite sure what kind of link you are expecting in this answer? – Marcus Riemer Oct 11 '16 at 13:24
  • 1
    This answer would be great if it included a plunkr – Zze May 20 '17 at 02:24
  • `Do not import any of the RouterModule.forChild() in the child modules.` This line is saved me............... :) – Ritwick Dey Jun 06 '18 at 11:35
  • Dosent this break the concept of modules. SInce if i dont want a child module, all i have to do is to remove the module references.But with this approach i have remove the module references and also removed the exported routes from the parent route config. – radio_head Sep 17 '18 at 14:15
35

I had the same problem.

The answer here is pretty good using loadChildren :

          {
             path: 'mypath',
             loadChildren : () => myModule
          }

https://github.com/angular/angular/issues/10958

Jklf
  • 467
  • 4
  • 4
  • 3
    That link is particularly helpful. – Zze May 20 '17 at 02:24
  • This is better than the accepted answer. Here, I can leave the sub-components in the module without having to expose them to the parent module. – mmey Feb 15 '19 at 06:41
  • I have a lot of problems using this. It works when you compile using `JIT` mode. But when you make a build on `AOT` mode simply doesn't work. AOT mode does not support this kind of module inicialization. – m.rufca May 30 '19 at 12:04
3

I think 2.0.0 rc5 isn't interested now. But is is worked in Angular 4, may be early too.

@NgModule({
    imports: [
        RouterModule.forRoot([
                {path: "", redirectTo: "test-sample", pathMatch: "full"},
                {
                    path: "test-sample",
                    loadChildren: () => TestSampleModule
                }
        ])
    ],
    exports: [RouterModule],
    declarations: [] 
}) 
export class AppRoutingModule{}

@NgModule({
    imports: [
        RouterModule.forChild([{
                    path: "",
                    component: TestSampleComponent,
                    children: [
                        {path: "", redirectTo: "home", pathMatch: "full"},
                        {
                            path: "home",
                            loadChildren: () => HomeModule
                        },
                        {
                            path: "about",
                            loadChildren: () => AboutModule
                        }
                    ]
                }
        ])
    ],
    exports: [RouterModule],
    declarations: []
})
export class TestSampleRoutingModule {}

@NgModule({
    imports: [RouterModule.forChild([{
                    path: "",
                    component: AboutComponent
                }
    ])],
    exports: [RouterModule]
})
export class AboutRoutingModule {}

Take in account on loadChildren: () => {...}. It is not the lazy loading.

See for details feat: Support NgModules hierarchy sans Lazy Loading

1

I found a way to resolve this as well. Basically, I am defining my routes the way that I used to, but this time at the top child level. For example my admin route:

const AdminRoutes: Routes = [
   {
      path: 'admin',
      component: AdminComponent,
      children: [
          ...setupRoutes
      ]
   }
]

export const adminRouting = RouterModule.forChild(AdminRoutes)

My setup routes file is imported from a sub area, which defines routes of it's own, including more children. The catch is that this file exports the "Routes" object and not the RouterModule.forChild result.

After that is setup, I removed the child and sub-child routes from the submodule definitions. I then had to export all of the components used in the routes, from each of the submodules, just like Marcus mentioned above. Once I did that, the routes started working just like I wanted them to.

I don't think I really like this solution since my parent module knows too much about the child module routes. But at least its an easy way to get around it for RC5, and it doesn't leak all of my components all over the place. I'll go ahead and mark Marcus' answer as the answer since he put me on the right track.

John Ackerman
  • 1,021
  • 1
  • 8
  • 15
  • Hi John, your approach does work, however how do you deal with module lazy loading? It seems with this approach, you can't lazy load other modules – Alex G Jan 31 '17 at 08:15
1

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 .

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

When you are using ngModules and RC5, the routing configuration of your parent module does not need to know anything about the child modules routing. You only have to define the routes for your parent module here. Furthermore you have to import the child module into your parent module. In the child module you have to define your routes this way:

export const childRoutes: Routes = [
  {
    path: 'someChild',
      component: SomeChildComponent,
      children: [
        { path: '', component: SomeChildSubComponent1 },
        { path: 'comp1', component: SomeChildSubComponent1 },
        { path: 'comp2', component: SomeChildSubComponent2 }
      ]
  }
];

This will let you have a url like /someParent/someChild/comp1 - and the components are displayed in their corresponding router-outlet. Please note: You HAVE TO declace a component for the empty path. Otherwise you are not able to navigate to you children.

westor
  • 1,116
  • 1
  • 11
  • 31
  • 1
    This only works for the first level of nesting. For all subsequent levels the routes are added to the root module and unreachable through their parent. – Thorsten Westheider Sep 22 '16 at 03:15
  • High Torsten, unfortunately you are right. You could reach your component, when you modify the path information of the routes object by prepending the parents path. But you still will not see it in the correct router-outlet. That seems to be also the problem with your solution. May be it is a bug? Do you know, if there is an issue open for this already? – westor Sep 22 '16 at 15:30
  • I think it's by design, you can work around it using lazy loading and that' s probably a better solution anyway. – Thorsten Westheider Sep 22 '16 at 20:26
  • Any progress for this issue? I just faced the same problem with version 2.0.0. @ThorstenWestheider – Kinorsi Oct 10 '16 at 09:28
0

Use loadChildren. That's OK. But when you restart the build process, you will get the build error. You should match the exported symbol to the loadChildren.

import { ChildModule } from './child/child.module';

export function childRouteFactory() {
  return ChildModule;
}

...
  {
    path: 'child',
    loadChildren: childRouteFactory
  }
...
Jehong Ahn
  • 672
  • 7
  • 20