Angular-cli: Add loaded routes from REST in runtime

Created on 25 Jan 2017  Â·  38Comments  Â·  Source: angular/angular-cli

There's a bug when adding routes dynamically using router.resetConfig() method. I'm not sure if it's a bug in Angular-CLI, Angular compiler, Angular Router or Webpack, please guide me.

This is the setup:

## routes.json
[
    { "path": "contact", "loadChildren": "./contact/contact.module#ContactModule" }
]

## RouterService.ts
getRotues() {
   return this._http.get('routes.json')
     .map((res) => res.json())
}

Now to the actual bug:

let routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full'},
  { path: 'home', loadChildren: './home/home.module#HomeModule' }
];

@NgModule({
  imports: [ RouterModule.forRoot(routes) ]
})

export class AppModule {
  constructor(private router:Router, private routerService:RouterService) {
    // This works
    // Pushing the same route as in routes.json
    // ----------------------
    routes.push({ path: 'contact', loadChildren: './contact/contact.module#ContactModule' })
    router.resetConfig(router);

    // This does not work
    // Loads json data from service and push the new route
    // ----------------------
    this.routerService.getRoutes().subscribe((result) => {
      result.forEach((route) => {
        routes.push({ path: route.path, loadChildren: route.loadChildren })
      });
      this.router.resetConfig(routes);
    });

    // This does works
    // By just console log this anonymous object, the loaded routing from the 
    // json data will work. 
    // ----------------------
    console.log({ loadChildren: './contact/contact.module#ContactModule' })
    this.routerService.getRoutes().subscribe((result) => {
      result.forEach((route) => {
        routes.push({ path: route.path, loadChildren: route.loadChildren })
      });
      this.router.resetConfig(routes);
    });
  }

Please provide us with the following information:

OS?

Windows 7

Versions.

Please run ng --version. If there's nothing outputted, please run in a Terminal: node --version and paste the result here:
angular-cli: 1.0.0-beta.21 (Also tried with Angular-cli 1.0.0-beta.26 with same result)
node: 5.8.0
os: win32 x64

Repro steps.

The app was created by Angular-CLI, setup already described.

The log given by the failure.

Error: Uncaught (in promise): Error: Cannot find module './contact/contact.module'.
Error: Cannot find module './contact/contact.module'.
at webpackAsyncContext (main.bundle.js:64815)
at SystemJsNgModuleLoader.loadAndCompile (main.bundle.js:73204)
at SystemJsNgModuleLoader.load (main.bundle.js:73196)
at RouterConfigLoader.loadModuleFactory (main.bundle.js:20568)
at RouterConfigLoader.load (main.bundle.js:20559)
at MergeMapSubscriber.project (main.bundle.js:76506)
at MergeMapSubscriber._tryNext (main.bundle.js:16175)
at MergeMapSubscriber._next (main.bundle.js:16165)
at MergeMapSubscriber.Subscriber.next (main.bundle.js:440)
at ScalarObservable._subscribe (main.bundle.js:41699)
at webpackAsyncContext (main.bundle.js:64815)
at SystemJsNgModuleLoader.loadAndCompile (main.bundle.js:73204)
at SystemJsNgModuleLoader.load (main.bundle.js:73196)
at RouterConfigLoader.loadModuleFactory (main.bundle.js:20568)
at RouterConfigLoader.load (main.bundle.js:20559)
at MergeMapSubscriber.project (main.bundle.js:76506)
at MergeMapSubscriber._tryNext (main.bundle.js:16175)
at MergeMapSubscriber._next (main.bundle.js:16165)
at MergeMapSubscriber.Subscriber.next (main.bundle.js:440)
at ScalarObservable._subscribe (main.bundle.js:41699)
at resolvePromise (main.bundle.js:101365)
at resolvePromise (main.bundle.js:101350)
at main.bundle.js:101399
at ZoneDelegate.invokeTask (main.bundle.js:101162)
at Object.onInvokeTask (main.bundle.js:36074)
at ZoneDelegate.invokeTask (main.bundle.js:101161)
at Zone.runTask (main.bundle.js:101051)
at drainMicroTaskQueue (main.bundle.js:101298)
at HTMLElement.ZoneTask.invoke (main.bundle.js:101236)

Mention any other details that might be useful.


Thanks! We'll be in touch soon.

3 (nice to have) RFC / discussion / question

Most helpful comment

So there is no plan to support such feature from AngularCLI level?
I am writing big enterprise app and such feature is crucial for me.

All 38 comments

This is a feature request for the compiler internal API (in Angular repo, mostly maintained by CLI people). I'm not expecting it to happen any time soon I'm afraid.

Let me explain how I understand this process works, having worked on the implementation of the first two steps myself recently:

  • In build time, the compiler goes by NgModule decorator to find all the loadChildren lazy module file paths so that Webpack can generate separate bundles for them later.

  • It also stores a mapping between the loadChildren strings and the actual file paths and symbols (module class names)

  • Some webpack magic happens and translates the file paths to the correct bundle files

  • In runtime, the router gets a map, where the keys are the exact strings in loadChildren and the values are the file paths after transformation to bundles and of course class names

The problem in your scenario is that none of the build steps happened. The router doesn't know where to load the files from except in a SystemJS-like way (resolving a URL relative to root), and there are no files to match that in output folder (/dist).

The compiler needs a static way of knowing about the loadChildren entries so that it can create the mapping above, and also create separate bundles for the lazy-loaded routes.

@Meligy hit the nail right on the head. I'm just going to tag this.

@Meligy @filipesilva this means that it's not possible to add routes dynamically to the router? Should I post this on Angular github page?

It's not possible for us to detect it using static analysis, since it depends on runtime data.

This is the right repo to have it as an feature request though, as the CLI chooses to enforce static analysis on all builds, and is the one discovering existing ones.

Perhaps we can provide a facility to manually list additional lazy NgModules to be provided to the build system, and add them to the list of routes already discovered via static analysis.

@hansl what do you think?

If routes are known ahead of time, another option is to include them in the router configuration of the app and then use a guard (https://angular.io/docs/ts/latest/guide/router.html#!#guards) to control activation and/or loading.

Any progress on this?

I don't see there being a way to have lazy route detection and AOT together with dynamic runtime lazy routes. You'll need a different build system than the one the CLI provides for that.

SystemJS would allow something like that, but probably not with AOT.

So there is no plan to support such feature from AngularCLI level?
I am writing big enterprise app and such feature is crucial for me.

The same here

same here

@filipesilva I wonder what's the relation between this thread and https://github.com/angular/angular/issues/11437. In that thread, people claim to have loaded routes dynamically. But I guess, AOT is out of the question.

same issue. with webpack3.4.1 + ng4.3.1

Is there a fix for this? Am totally stuck on this issue trying to just load my routing config from a json file.

@EvanBurbidge I am using sort of a not so clean hack to overcome this. The problem I noticed is that, loader needs to know in advance about all available modules. even if you don't know what paths your modules will map to, you are needed to tell about the all available modules.

So I what I do is that I map all my modules to dummy paths, and then cleanup/remove the dummy paths from route config array using resetConfig() when actual route config has been received from an api, in your case it will be json file. Not sure it will help in your case, but just sharing if it lets you move on for now, while a better solution becomes available.

I have the same problem, the routes need to come from a CMS. I wanted to have these defined inside index.html and then load them into the routerModule, alternatively loading them all from a separate file would work. Is there no way to do this?

same problem

same problem

The only way I've found is to have a basic router setup in my ts code and then at a later stage grab the full config from some json so webpack knows to load the stuff.

This boils down to webpack code splitting, as it prevents the loading of angular modules as OSGI bundles. LoadChildren of Angular Router and Webpack are related, as the code splitting (chunk files) happens there and it forces to provide the information at the build time. Didn't find a solution yet to achieve the true pluggable feature similar to how it is been achieved by using AMD, without re-building. For enterprise application(s) this is one of the significant requirement.. Any idea?

Is there anyone found out a way to resolve the above bug? I was trying to create a independent module which reads the routes from the other lazy loading modules. Please post the suggestions. Thank you!

I created a component that dynamically loads other components. I have data loaded from a CMS and the component type is included in that data, when it loads I destroy the old component and create a new one and pass in the data. I have a single wildcard route that always loads of dynamic component and I use the route to tell the server what data I need. It works pretty well and means I don’t have to specify and of the routes in angular at all, they are all managed in the CMS.

@Beavykins are you loading the components Lazily? Can you explain how did you exactly achieve that?

@filipesilva : @Meligy Can we compile the lazy loaded modules using AOT and read them from the JSON file?

@NaveenTula90 no the components are compiled as part of the app, the dynamic component has references to these components so I can load them in when needed. It’s only the data I am lazy loading and that contains the same reference to the required component. For me it’s way too limiting to have all the routes hard coded as the CMS can let them change routes and use any component anywhere.

@Beavykins The thing which @NaveenTula90 saying is for a large enterprise app, where the developer would give the routes and path to the component in a JSON file rather than messing with the modules.
If you are talking about having reference to the component, I don't think so we can have that many references...

@ramasamykasiviswanathan I guess the principle is the same though. I am getting the data for the route and a reference to the component that is going to be used, it’s just a key in the object that contains the reference to the real component. Instead of that it it could just be JSON with the routes and the component to use and refer to that when the route changes. But if you are also loading data from somewhere else to populate the component why not just pass in the component with the route data. I guess the downside is that you have to wait for the data before you can show the right component that way, but I have an animation on route change so you don’t see the delay.

@Beavykins Can you share a sample code so that we can get better idea on what you were saying? just a snippet would be fine? because it is going to be the base for enterprise level large application.

@mecp could you share a sample code? I think your solution would be the best but I can't get it to work. below is my code. thanks very much!

import { Shemesh4Component } from './shemesh4/shemesh4.component';
import { RouterModule, Routes, Router } from '@angular/router';
let routes: Routes = [
  {
    path: 'dummy-path',
    loadChildren: '../shemesh5/shemesh5.module#Shemesh5Module'
  },
  {
    path: '',
    loadChildren: '../shemesh6/shemesh6.module#Shemesh6Module'
  }

];


@NgModule({
  imports: [
    CommonModule, RouterModule.forChild(routes)
  ],
  declarations: [Shemesh4Component]
})
export class Shemesh4Module {
  constructor(private router: Router) {
    // This works
    // Pushing the same route as in routes.json
    // ----------------------
    routes[0].path = 'realpath-fromconfig';
    router.resetConfig(routes);
  }
}

I am also try to do the same but no luck. map is not generated while push routes from json

If id do below code and publish the build it's work fine but same file read from JSON map not generated...

const routes = [
{
path: 'about',
loadChildren: 'app/components/about/about.module#AboutModule'
}
{
path: 'about',
loadChildren: 'app/components/about/about.module#AboutModule'
},
{
path: 'home',
loadChildren: 'app/components/home/home.module#HomeModule'
},
{
path: 'external',
loadChildren: 'app/components/external/external.module#ExternalModule'
}
];

ok, take a look here:
https://github.com/sharpangles/platform/tree/master/modules/angular-dynamic

this is exactly what everyone here needs. thanks @sharpangles !

@lmessinger
Could you help me out how we can bootstrap the repo shared by you.

Appreciate your quick help.

@lmessinger

Also any working demo for the above scenario would be fine where in routes are getting activated dynamically!

the documentation explains it. In essence the loading is throught an html
template that lists the components to be lazy loaded

On Wed, Nov 8, 2017 10:06 AM, rahul sahay [email protected] wrote:
@lmessinger

Also any working demo for the above scenario would be fine where in routes are
getting activated dynamically!

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

Lior Messinger
+1-646-3730044+972-546-888401

you need to invoke AOT compiler at runtime, somehow... like the webpack loader and plugin does at build time

@mecp
Can you provide your solution, how you achieved the same. It will be great help.

Thanks,
Rahul

I have a possible solution in my mind and want to share it with you. Our scenario is that our lazy routes come from other NPM packages within the node_modules folder.

  • Put all your packages which provide new lazy routes into one NPM namespace via the @annotation e.g. @company-name/lazy-modules/module-name
  • Put the lazy route information into the modules package.json as meta info (see https://stackoverflow.com/a/27232456/2664516)
  • Write a small Webpack plugin which goes through your NPM namespace, collect all the routing information and write a app.routes.ts file with all the static routes in it

With that you end up with proper static routes and decoupled modules with the benefit of being flexible by adding/removing NPM packages as you wish without the need of touching your code by hand.

I have the same requirement. Will it work if I put in my app routes just a single route to a lazyloaded module. This module will just be a slim dummy module (with an obscure name and always guarded) that has also routes that I basically use to populate and build all my other modules that I want lazyloaded? Then at runtime I can pull down my new app routes and build my actual routes dynamically. This way all my modules are built. Im wondering if something like this would work.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings