2

I want to load routes for the @RouteConfig Dynamically from a service which fetches in format JSON,

[
  { "path" : "/about" , "name" : "About" , "component" : "AboutComponent" },
  { "path" : "/contact" , "name" : "Contact" , "component" : "ContactComponent" }
]

Following is the code for pushing it into the RouteDefinition Array,

for (let i = 0; i < data.length; i++) {
  //console.log(data[i].path+" "+data[i].name+" "+data[i].component);
  this.routeConfigArray.push({ //routeConfigArray : RouteDefinition
    'path': data[i].path,
    'name': data[i].name,
    'component': data[i].component
  });
  this._router.config(this.routeConfigArray);   // THIS FAILS TO CONFIG PATHS ON ROUTER
}

The 'component':data[i].component requires Classname, where as it recieves a it via a String class. How do i convert the string containing classname into a class??

Also i have tried the Route class using :

this.routeConfigArray.push(new Route({path: data[i].path, name: data[i].name, component:data[i].component}));

Console Error:

Component for route "/about" is not defined, or is not a class. i have tried numerous ways like using eval("new "+data[i].component+"()); || new window[data[i].component] .

I am stuck at this and really confused as to how to resolve this.

dab0bby
  • 2,951
  • 1
  • 31
  • 35
Pratik Kelwalkar
  • 1,592
  • 2
  • 15
  • 21
  • Have you tried `eval(data[i].component)`? The problem with this, besides the usage of `eval()` on external data, is that it needs the `import` statement of this component as well `import {AboutComponent} from "path/to/component/about.component";`. And i am not sure how you would be able to do this.. – Poul Kruijt Mar 09 '16 at 09:15
  • @PierreDuc I do have the `import {AboutComponent} from './about/about.component';` But doing `var testClassName =eval(data[i].component);` cause the error **AboutComponent is not defined** – Pratik Kelwalkar Mar 09 '16 at 09:20
  • Well, try the `eval(data[i].component)` :) – Poul Kruijt Mar 09 '16 at 09:21
  • Im sorry @PierreDuc but it results into the same thing, lets say i do a `var testClassName = new AboutComponent(); console.log(testClassName.constructor.name)` Outputs correctly to **AboutComponent** instead of the previous undesired version **string** – Pratik Kelwalkar Mar 09 '16 at 09:24
  • Ah, i understand why `eval` is not working. If you look at the generated `js`, you can see that it imports `AboutComponent` probably something like `about_component_1.AboutComponent`. So if you can convert the camelCase to underscore. Make it all lowercase, and add a `_1`. Put that result in a variable named, `something`. You can do `eval(something[data[i].component])`. Obviously the compiled javascript is subject to change, and this is probably a very bad approach, but you can try :D. Although, the compiler probably doesn't create that variable, because you don't really use that Component – Poul Kruijt Mar 09 '16 at 09:34
  • @PierreDuc you are absolutely right!!!!!...thanku so much for pointing it out. it does get compiled to ` about_component_1.AboutComponent ` . But now in the scenario u have suggested the `something` must be hardcoded for every such component, i do not want that, i know the imports must be present but the flow of what routes to be loaded must be dynamic from a existing set of routes. I hope you got my scenario. Thanks again! :) – Pratik Kelwalkar Mar 09 '16 at 09:49
  • I am going to post something as an answer to be able to add real code, maybe it will help you – Poul Kruijt Mar 09 '16 at 09:53
  • thankyou so much @PierreDuc +1...really gr8 of u... :) – Pratik Kelwalkar Mar 09 '16 at 09:56

4 Answers4

1

TypeScript compiles imports into javascript in a certain way, you can try something like this to be able to use eval() (cringe). This obviously won't work if typescript compiles differently, but it is fun to check out if this works any ways :)

for (let i = 0; i < data.length; i++) {
  this.routeConfigArray.push({ //routeConfigArray : RouteDefinition
    path: data[i].path,
    name: data[i].name,
    component: getComponent(data[i].component)
  });
  this._router.config(this.routeConfigArray);   
}

function getComponent(comp : string) : Function {
    //convert camelCase to underscore notation
    let component : string = comp;
    component = component[0].toLowerCase() + component.slice(1);
    component = component.replace(/([A-Z])/g, function(match) {
        return '_' + match.toLowerCase();
    });
    component += '_1';
    return eval(component[comp])
}

ADDENDUM

As addition to your own solution with using a AsyncRoute, I believe you actually got quite a good solution going on. Perhaps if you place all the pages in a certain way, you can extract the resource location from the path, but that is not necessary. (i mean to get from the path string /about to the resource string ./app/about/about.component shouldn't be hard with a small algorithm. But that might be something for an update.

Anyways, you can try something like this with the AsyncRoute

warning: untested code ahead

let routes : any[] = [
    { "path" : "/about" , "name" : "About" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" },
    { "path" : "/contact" , "name" : "Contact" , "component" : "ContactComponent", "route": "/Contact" , "resource" : "./app/contact/contact.component" }
];

routes.forEach((route : any) => {
    this.routeConfigArray.push(
        new AsyncRoute({
            path : route.path,
            loader : () => System.import(route.resource).then(m => m[route.component]),
            name : route.name
        })
    );
});

this._router.config(this.routeConfigArray);
Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
1

You are too good @PierreDuc , i was just looking at regex to build the same function, some edits i would like to point out to bring it in working state....

for (let i = 0; i < data.length; i++) {
  this.routeConfigArray.push({ //routeConfigArray : RouteDefinition
    'path': data[i].path,
    'name': data[i].name,
    'component': getComponent(data[i].component).constructor
  });
  this._router.config(this.routeConfigArray);   
}

function getComponent(comp : string) : Function {
    //convert camelCase to underscore notation
    let component : string = comp;
    component = component[0].toLowerCase() + component.slice(1);
    component = component.replace(/([A-Z])/g, function(match) {
        return '_' + match.toLowerCase();
    });
    component += '_1.';
    return eval("new "+component+comp+"()")
}

Thankyou once again dude, its now in a running mode!!! Phew!

Pratik Kelwalkar
  • 1,592
  • 2
  • 15
  • 21
  • 1
    You are welcome! Glad i could help. Although i would expect that `eval(component[comp])` would already return the constructor and that creating it first with `new` and then get the constructor is overkill.. :) – Poul Kruijt Mar 09 '16 at 15:05
1

You could use the another approach that the one from @Pierre and @Pratik based on a method that returns the name of classes:

Object.prototype.getName = function() { 
  var funcNameRegex = /function (.{1,})\(/;
  var results = (funcNameRegex).exec((this).constructor.toString());
  return (results && results.length > 1) ? results[1] : "";
};

In your component you can then configure your routes dynamically like this:

ngOnInit() {
  this.routes = [
    {
      path: '/test', component: 'OtherComponent', name: 'Test'
    }
  ];
  this.configureRoutes(this.routes);
  this.router.config( this.routes);
}

configureRoutes(routes) {
  var potentialComponents = [ OtherComponent ];
  routes.forEach((route) => {
    route.component = potentialComponents.find((component) => {
      return component.name === route.component;
    });
  });
}

This requires to know by advance potential components that can be involved in routing.

See this plunkr for demo: https://plnkr.co/edit/KKVagp?p=preview.

See this question:

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • awesome! ..the plunkr example helped a lot. Using angular 2 , takes me back to school! ... – Pratik Kelwalkar Mar 09 '16 at 11:04
  • Hey as based on everyone's input... Dynamic routing wont be completely dynamic as **have to use import {ComponentName} from 'path/to/component** or some place other in the code like mentioned above in potentailComponents right? But i have almost found a way without using any imports or mentioning the Component in directive or anywhere else, using AsyncRoute i have almost made it, but again stuck badly, would post code, any help would be appreciated – Pratik Kelwalkar Mar 10 '16 at 13:20
  • How do you load 2 new routes? I have this plunkr: https://plnkr.co/edit/gEG6TJ?p=preview – crh225 Aug 03 '16 at 21:33
0

Update to the first above scenario : The above dynamically loads routes on the UI, but for that to happen i do have to mention it in my AppComponent

import {AboutComponent} from '/path';
import {ContactComponent} from '/path';

//and then either include it in a directive or mention The component Name some place in the code

But this defeats the purpose 'Dynamic Loading', as i have to know which components would be requested and also cannot lazy load them. Say i do that for 500 components, i have to load these 500 components and then just from a JSON mentioned above i pick which has to be loaded on the UI.

Solution(Completed And Tested) ==> I now do not have to mention the components i want to load in any import statement neither in any Directive. Thererby making it lazy loading and Completely Dynamic

How By Using AsyncRoute Class.

Here's my new JSON
//"route" is to map it to [routerLink]="['/Home']" & "resource" is the actual path of component

 [
  { "path" : "/about" , "name" : "About" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" },
  { "path" : "/contact" , "name" : "Contact" , "component" : "ContactComponent", "route": "/About" , "resource" : "./app/about/about.component" },
  { "path" : "/blog" , "name" : "Blog" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" },
  { "path" : "/news" , "name" : "News" , "component" : "AboutComponent", "route": "/About" , "resource" : "./app/about/about.component" }
]

Now to the code, here i fetch this JSON and add it to the routeConfigArray : RouteDefinition[] and call the Router's .config(routeConfigArray)

  let routes : any[] = data; // consist the json data in array


routes.forEach((route : any) => {
        this.routeConfigArray.push(
            new AsyncRoute({
                path : route.path,
                loader : () => System.import(route.resource).then(m => m[route.component]),
                name : route.name
            })
        );
    });

    this._router.config(this.routeConfigArray);

And thats how it works!!!

Pratik Kelwalkar
  • 1,592
  • 2
  • 15
  • 21