Dynamically creating routes in Angular JS
We are trying to make a switch to angular, but we have a pretty big routing problem. Our current site has something like 10,000 unique routes - each page has a unique ".html" ID. There is no specific convention that would allow us to assign a controller to them, so I created a search API endpoint.
Here's the workflow I'm trying to create:
-
Angular application loading. One route is configured differently.
-
When someone clicks on a link, I don't know if the resource is a product or a category, so the search endpoint request is made with the unique identifier ".html". The endpoint returns two things: the resource name and the identifier (for example, "product" and "10"). So to be clear, they ended up on a page like " http://www.example.com/some-identifier.html ," I am querying the search API to find out what resource it is and get a result like "product" and " 10 "- now I know its product controller / template and I need data from product id 10.
-
The application assigns a controller and template ("productController" and "product.html"), requests the correct endpoint for the data ("/ api / product / 10"), and renders the template.
Problems I am facing:
-
$ http is not available during configuration, so I cannot get into the lookup table.
-
Adding routes after config is sloppy at best - I did it successfully by assigning $ routeProvider to a global variable and doing it after the fact, but man, this is ugly.
-
Loading all routes seems impractical - the file size will be quite heavy for a lot of connections / browsers.
-
We cannot change the agreement now. We have 4 years of SEO and a lot of organic traffic to get rid of our URLs.
It seems to me that I am thinking about this the wrong way and there is something missing. The lookup table is really a problem - not knowing which resource to download (product, category, etc.). I read this article about loading routes dynamically, but again, it doesn't make an external request. For us, loading controllers is not a problem, it resolves routes and then assigns them c
How would you solve the problem?
Decision
Thank you so much @ user2943490 for pointing me in the right direction. Don't forget to boost your answer! I made it a little more general so that I don't need to define route types.API structure
This configuration requires at least two endpoints: /api/routes/lookup/:resource_to_lookup:/
and /api/some_resource_type/id/:some_resource_id:/
. We request a lookup to find out what resource it points to and what the resource ID is. This allows you to have nice clean URLs like: " http://www.example.com/thriller.html " (one) and " http://www.example.com/michaeljackson.html " (collection).
In my case, if I ask for something like "awesome_sweatshirt.html", my search will return a JSON object with "{type: 'product', id: 10}". Then I ask for "/ api / product / id / 10" to get the data.
"Isn't it that slow?" you ask. With the varnish on the front, it all happens in less than 1 second. We can see that the pageload time locally is less than 20ms. Through the wire from the slow server-to-server was closer to half a second.
app.js
var app = angular.module('myApp', [
'ngRoute'
])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.otherwise({
controller: function($scope, $routeParams, $controller, lookupService) {
/* this creates a child controller which, if served as it is, should accomplish your goal behaving as the actual controller (params.dashboardName + "Controller") */
if ( typeof lookupService.controller == "undefined" )
return;
$controller(lookupService.controller, {$scope:$scope});
delete lookupService.controller;
//We have to delete it so that it doesn't try to load again before the next lookup is complete.
},
template: '<div ng-include="templateUrl"></div>'
});
$locationProvider.html5Mode(true);
})
.controller('appController', ['$scope', '$window', '$rootScope', 'lookupService', '$location', '$route', function($scope, $window, $rootScope, lookupService, $location, $route){
$rootScope.$on('$locationChangeStart', handleUniqueIdentifiers);
function handleUniqueIdentifiers (event, currentUrl, previousUrl) {
window.scrollTo(0,0)
// Only intercept those URLs which are "unique identifiers".
if (!isUniqueIdentifierUrl($location.path())) {
return;
}
// Show the page load spinner
$scope.isLoaded = false
lookupService.query($location.path())
.then(function (lookupDefinition) {
$route.reload();
})
.catch(function () {
// Handle the look up error.
});
}
function isUniqueIdentifierUrl (url) {
// Is this a unique identifier URL?
// Right now any url with a '.html' is considered one, substitute this
// with your actual business logic.
return url.indexOf('.html') > -1;
}
}]);
lookupService.js
myApp.factory('lookupService', ['$http', '$q', '$location', function lookupService($http, $q, $location) {
return {
id: null,
originalPath: '',
contoller: '',
templateUrl: '',
query: function (url) {
var deferred = $q.defer();
var self = this;
$http.get("/api/routes/lookup"+url)
.success(function(data, status, headers, config){
self.id = data.id;
self.originalPath = url;
self.controller = data.controller+'Controller';
self.templateUrl = '/js/angular/components/'+data.controller+'/'+data.controller+'.html';
//Our naming convention works as "components/product/product.html" for templates
deferred.resolve(data);
})
return deferred.promise;
}
}
}]);
productController.js
myApp.controller('productController', ['$scope', 'productService', 'cartService', '$location', 'lookupService', function ($scope, productService, cartService, $location, lookupService) {
$scope.cart = cartService
// ** This is important! ** //
$scope.templateUrl = lookupService.templateUrl
productService.getProduct(lookupService.id).then(function(data){
$scope.data = data
$scope.data.selectedItem = {}
$scope.$emit('viewLoaded')
});
$scope.addToCart = function(item) {
$scope.cart.addProduct(angular.copy(item))
$scope.$emit('toggleCart')
}
}]);
source to share
Try something like this.
In the route configuration, you configured the definition for each resource type and their controllers, templates, and permissions:
$routeProvider.when('/products', {
controller: 'productController',
templateUrl: 'product.html',
resolve: {
product: function ($route, productService) {
var productId = $route.current.params.id;
// productService makes a request to //api/product/<productId>
return productService.getProduct(productId);
}
}
});
// $routeProvider.when(...
// add route definitions for your other resource types
Then you listen $locationChangeStart
. If the URL you're navigating to is a "unique identifier", request a search. Depending on the type of resource returned by the search, navigate to the correct route above.
$rootScope.$on('$locationChangeStart', handleUniqueIdentifiers);
function handleUniqueIdentifiers (event, currentUrl, previousUrl) {
// Only intercept those URLs which are "unique identifiers".
if (!isUniqueIdentifierUrl(currentUrl)) {
return;
}
// Stop the default navigation.
// Now you are in control of where to navigate to.
event.preventDefault();
lookupService.query(currentUrl)
.then(function (lookupDefinition) {
switch (lookupDefinition.type) {
case 'product':
$location.url('/products');
break;
case 'category':
$location.url('/categories');
break;
// case ...
// add other resource types
}
$location.search({
// Set the resource ID in the query string, so
// it can be retrieved by the route resolver.
id: lookupDefinition.id
});
})
.catch(function () {
// Handle the look up error.
});
}
function isUniqueIdentifierUrl (url) {
// Is this a unique identifier URL?
// Right now any url with a '.html' is considered one, substitute this
// with your actual business logic.
return url.indexOf('.html') > -1;
}
source to share
You can use $ routeParams for this.
eg.
route/:type/:id
so the type and id can be completely dynamic, different type handling will be up to the route controller.
source to share
What if you have a json file with route information (and if there is no security issue) and iterate over it to attach routes to the application?
eg.
JSON:
routes: [
{
controller: "Controller1"
path: "/path1"
templateUrl: 'partials/home/home.html'
},
{
controller: "Controller1"
path: "/path1"
templateUrl: 'partials/home/home.html'
}
]
And then iterate over the JSON content and join them to $routeProvider.when
? I'm not sure if this is a good idea, it depends on how large the JSON file is going to be and if you don't want to expose all your routes to a potential attacker.
source to share
From AngularJS documentation ,
The $ routeParams service allows you to get the current set of route parameters.
Dependencies:
$route
An example looks like
// Given:
// URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
// Route: /Chapter/:chapterId/Section/:sectionId
// Then
$routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'}
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
function $RouteParamsProvider() {
this.$get = function() { return {}; };
}
source to share