We're trying to make the switch to angular, but we have a pretty big issue with routing. Our current site has something like 10,000 unique routes -- ever page has a unique ".html" identifier. There's no particular convention that would allow us to assign the controller to them, so I created a lookup API endpoint.
Here's the workflow I'm trying to create:
Angular app loads. One "otherwise" route is set up.
When someone clicks a link, I don't know if the resource is a product or a category, so a query is made to the lookup endpoint with the unique ".html" identifier. The endpoint returns two things: the name of the resource, and an ID ("product" and "10" for example). So to be clear, they hit a page like, "http://www.example.com/some-identifier.html," I query the lookup API to find out what kind of resource this is, and get a result like, "product" and "10" -- now I know it's the product controller/template and I need the data from product id 10.
The app assigns the controller and template ("productController" and "product.html"), queries the correct endpoint for data ("/api/product/10"), and renders the template.
The problems I'm facing:
$http isn't available during config, so I can't hit the lookup table.
Adding routes after the config is sloppy at best -- I've done it successfully by assigning $routeProvider to a global variable and doing it after the fact, but man, it's ugly.
Loading all the routes seems impractical -- just the size of the file would be pretty heavy for a lot of connections/browsers.
We can't change the convention now. We have 4 years of SEO and a lot of organic traffic to abandon our URLs.
I feel like I might be thinking about this the wrong way and there's something missing. The lookup table is really the problem -- not knowing what kind of resource to load (product, category, etc). I read this article about loading routes dynamically, but again, he's not making an external query. For us, loading the controllers isn't the problem, it's resolving the routes and then assigning them c
How would you solve the problem?
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 query the lookup to find out what kind of resource it points to and what the ID of the resource is. This allows you to have nice clean urls, like, "http://www.example.com/thriller.html" (a single) and "http://www.example.com/michaeljackson.html" (a collection).
In my case, if I query something like, "awesome_sweatshirt.html" my lookup will return a JSON object with "{type: 'product', id: 10}". Then I query "/api/product/id/10" to get the data.
"Isn't that slow?" you ask. With varnish in front, all of this happens in way less than 1 second. We're seeing pageload times locally of less than 20ms. Across the wire from a slow dev 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')
}
}]);
Try something like this.
In the route config you set up a definition for each resource type and their controllers, templates and a resolve:
$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 for $locationChangeStart. If the URL being navigated to is a "unique identifer", query the lookup. Depending on the resource type returned by the lookup, navigate to the correct route as defined 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's 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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With