88

I currently have an AngularJS application with routing built in. It works and everything is ok.

My app.js file looks like this:

angular.module('myapp', ['myapp.filters', 'myapp.services', 'myapp.directives']).
  config(['$routeProvider', function ($routeProvider) {
      $routeProvider.when('/', { templateUrl: '/pages/home.html', controller: HomeController });
      $routeProvider.when('/about', { templateUrl: '/pages/about.html', controller: AboutController });
      $routeProvider.when('/privacy', { templateUrl: '/pages/privacy.html', controller: AboutController });
      $routeProvider.when('/terms', { templateUrl: '/pages/terms.html', controller: AboutController });
      $routeProvider.otherwise({ redirectTo: '/' });
  }]);

My app has a CMS built in where you can copy and add new html files within the /pages directory.

I would like to still go through the routing provider though even for the new dynamically added files.

In an ideal world the routing pattern would be:

$routeProvider.when('/pagename', { templateUrl: '/pages/pagename.html', controller: CMSController });

So if my new page name was "contact.html" I would like angular to pick up "/contact" and redirect to "/pages/contact.html".

Is this even possible?! and if so how?!

Update

I now have this in my routing config:

$routeProvider.when('/page/:name', { templateUrl: '/pages/home.html', controller: CMSController })

and in my CMSController:

function CMSController($scope, $route, $routeParams) {
    $route.current.templateUrl = '/pages/' + $routeParams.name + ".html";
    alert($route.current.templateUrl);
}
CMSController.$inject = ['$scope', '$route', '$routeParams'];

This sets the current templateUrl to the right value.

However I would now like to change the ng-view with the new templateUrl value. How is this accomplished?

Guillaume
  • 10,463
  • 1
  • 33
  • 47
Greg
  • 31,180
  • 18
  • 65
  • 85

7 Answers7

133
angular.module('myapp', ['myapp.filters', 'myapp.services', 'myapp.directives']).
        config(['$routeProvider', function($routeProvider) {
        $routeProvider.when('/page/:name*', {
            templateUrl: function(urlattr){
                return '/pages/' + urlattr.name + '.html';
            },
            controller: 'CMSController'
        });
    }
]);
  • Adding * let you work with multiple levels of directories dynamically. Example: /page/cars/selling/list will be catch on this provider

From the docs (1.3.0):

"If templateUrl is a function, it will be called with the following parameters:

{Array.} - route parameters extracted from the current $location.path() by applying the current route"

Also

when(path, route) : Method

  • path can contain named groups starting with a colon and ending with a star: e.g.:name*. All characters are eagerly stored in $routeParams under the given name when the route matches.
Guilherme Ferreira
  • 2,209
  • 21
  • 23
Robin Rizvi
  • 5,113
  • 4
  • 27
  • 35
  • 5
    Need to move with the times...the functionality I built that was missing in v1.0.2 is now available. Updating the accepted answer to this one – Greg May 22 '14 at 14:16
  • To be honest I never got this working with the current 1.3 betas – Archimedes Trajano Jul 15 '14 at 22:17
  • Actually got it working when I did this... when('/:page', { templateUrl : function(parameters) { return parameters.page + '.html'; } – Archimedes Trajano Jul 15 '14 at 22:36
  • Seriously.. It is really stupid that Angular does not have it as a default behavior ... That manual routing is ridiculous – Guilherme Ferreira May 06 '15 at 19:32
  • 3
    Please explain, Where from `urlattr` is set or sent, i mean what `urlattr.name` will produce? – Sami Jul 28 '15 at 04:45
  • How to use pattern in a controller name? – yu.pitomets Oct 29 '15 at 18:24
  • This solution it great to catch route for subdirectories, and should precise the subdirectory name. This will catch all routes from /: $routeProvider.when('/:any*', { templateUrl: function(params){ return params.any; }, controller: 'theController' }); – Yassine Khachlek Feb 11 '16 at 13:41
37

Ok solved it.

Added the solution to GitHub - http://gregorypratt.github.com/AngularDynamicRouting

In my app.js routing config:

$routeProvider.when('/pages/:name', {
    templateUrl: '/pages/home.html', 
    controller: CMSController 
});

Then in my CMS controller:

function CMSController($scope, $route, $routeParams) {

    $route.current.templateUrl = '/pages/' + $routeParams.name + ".html";

    $.get($route.current.templateUrl, function (data) {
        $scope.$apply(function () {
            $('#views').html($compile(data)($scope));
        });
    });
    ...
}
CMSController.$inject = ['$scope', '$route', '$routeParams'];

With #views being my <div id="views" ng-view></div>

So now it works with standard routing and dynamic routing.

To test it I copied about.html called it portfolio.html, changed some of it's contents and entered /#/pages/portfolio into my browser and hey presto portfolio.html was displayed....

Updated Added $apply and $compile to the html so that dynamic content can be injected.

Blaise
  • 13,139
  • 9
  • 69
  • 97
Greg
  • 31,180
  • 18
  • 65
  • 85
  • 2
    This works only with static content, if I understand it correctly. This is because you are altering the DOM after you've instantiated the controller, via jQuery (outside angular's scope). Variables like {{this}} may work (I would have to try it) but directives most likely won't. Be wary of this, as it may be usefull now, but can break code later.. – Tiago Roldão Dec 03 '12 at 11:29
  • True, binding doesn't work, for me however I'm not too fussed as I only want clients to be able to add new pages. Any pages I add I would specify appropriately via the route config. Good point though. – Greg Dec 03 '12 at 12:34
  • 1
    It's not a bad solution if you want to render static html content. So long as you bear in mind you are essentially overriding the ng-view with static (non angular) content. With that in mind, it would be cleaner to separate the two, creating something like a
    element, and injecting your "user template" there. Also, there is absolutely no sense in overriding $route.current.templateUrl - creating a $scope.userTemplate model and doing $('#user-views").load($scope.userTemplate) is cleaner, and breaks none of angular's code.
    – Tiago Roldão Dec 03 '12 at 12:46
  • Is there a better way of updating the `ng-view` element? `$('#views').load()` in my answer is valid but as you say doesn't get "evaluated" because its JQuery by-passing Angular... – Greg Dec 03 '12 at 14:13
  • 1
    No - the ng-view directive is rather limited - or better said, it is specific for use with the native routing system (which is, in turn, still limited, imho). You can do as you said, injecting the content into a DOM element, and then call the $compile method, to tell angular to reread the structure. That will trigger any bindings that need be done. – Tiago Roldão Dec 03 '12 at 14:47
  • Update my answer, compile didn't seem to work reliably so I wrapped it in an apply and that seems to have fixed it. Thanks for your help. – Greg Dec 04 '12 at 10:47
  • You are still mixing the ng-view directive with your manual code.. I wouldn't do that, doesn't seem very stable. But as it is, it does work. – Tiago Roldão Dec 04 '12 at 11:44
  • 2
    Works, but you shouldn't do DOM manipulation in your controller. – dmackerman Oct 14 '13 at 15:28
16

I think the easiest way to do such thing is to resolve the routes later, you could ask the routes via json, for example. Check out that I make a factory out of the $routeProvider during config phase, via $provide, so I can keep using the $routeProvider object in the run phase, and even in controllers.

'use strict';

angular.module('myapp', []).config(function($provide, $routeProvider) {
    $provide.factory('$routeProvider', function () {
        return $routeProvider;
    });
}).run(function($routeProvider, $http) {
    $routeProvider.when('/', {
        templateUrl: 'views/main.html',
        controller: 'MainCtrl'
    }).otherwise({
        redirectTo: '/'
    });

    $http.get('/dynamic-routes.json').success(function(data) {
        $routeProvider.when('/', {
            templateUrl: 'views/main.html',
            controller: 'MainCtrl'
        });
        // you might need to call $route.reload() if the route changed
        $route.reload();
    });
});
eazel7
  • 189
  • 1
  • 3
  • Aliasing `$routeProvider` to use as a `factory` (available after `config`) will not play well with security and app cohesion -- either way, cool technique (upvote!). – Cody May 16 '14 at 22:24
  • @Cody can you explain the security/app cohesion note? We're in a similar boat and something like the above answer would be really handy. – virtualandy Nov 05 '14 at 04:56
  • 2
    @virtualandy, it's generally not a great approach as you're making a provider available across different phases -- which in turn can leak high-level utils that should be concealed within the configuration phase. My suggestion is to employ UI-Router (takes care of a lot of difficulties), use "resolve", "controllerAs", and other faculties like setting templateUrl to a function. I also built a "presolve" module that I can share that can $interpolate any part of a route scheme (template, templateUrl, controller, etc). The above solution is a more elegant one -- but what are your major concerns? – Cody Nov 05 '14 at 08:38
  • 1
    @virtualandy, here is a module for lazyLoaded templates, controllers, etc -- apologies its not in a repo, but here's a fiddle: http://jsfiddle.net/cCarlson/zdm6gpnb/ ... This lazyLoads the above -- AND -- can interpolate anything based upon the URL parameters. The module could use some work, but its at least a starting point. – Cody Nov 05 '14 at 17:47
  • @Cody - no problem, thanks for the sample and further explanation. Makese sense. In our case, we're using [angular-route-segment](http://angular-route-segment.com/) and it's worked nicely. But we need to now support "dynamic" routes (some deploys may not have the full features/routes of the page, or may change their names, etc) and the notion of loading in some JSON that defines routes before actually implementing was our thought but ran into problems using $http/$providers within .config() obviously. – virtualandy Nov 05 '14 at 20:43
7

In the $routeProvider URI patters, you can specify variable parameters, like so: $routeProvider.when('/page/:pageNumber' ... , and access it in your controller via $routeParams.

There is a good example at the end of the $route page: http://docs.angularjs.org/api/ng.$route

EDIT (for the edited question):

The routing system is unfortunately very limited - there is a lot of discussion on this topic, and some solutions have been proposed, namely via creating multiple named views, etc.. But right now, the ngView directive serves only ONE view per route, on a one-to-one basis. You can go about this in multiple ways - the simpler one would be to use the view's template as a loader, with a <ng-include src="myTemplateUrl"></ng-include> tag in it ($scope.myTemplateUrl would be created in the controller).

I use a more complex (but cleaner, for larger and more complicated problems) solution, basically skipping the $route service altogether, that is detailed here:

http://www.bennadel.com/blog/2420-Mapping-AngularJS-Routes-Onto-URL-Parameters-And-Client-Side-Events.htm

Tiago Roldão
  • 10,629
  • 3
  • 29
  • 28
  • I love the `ng-include` method. Its really simple and this is a much better approach than manipulating the DOM. – Neel May 02 '14 at 20:53
5

Not sure why this works but dynamic (or wildcard if you prefer) routes are possible in angular 1.2.0-rc.2...

http://code.angularjs.org/1.2.0-rc.2/angular.min.js
http://code.angularjs.org/1.2.0-rc.2/angular-route.min.js

angular.module('yadda', [
  'ngRoute'
]).

config(function ($routeProvider, $locationProvider) {
  $routeProvider.
    when('/:a', {
  template: '<div ng-include="templateUrl">Loading...</div>',
  controller: 'DynamicController'
}).


controller('DynamicController', function ($scope, $routeParams) {
console.log($routeParams);
$scope.templateUrl = 'partials/' + $routeParams.a;
}).

example.com/foo -> loads "foo" partial

example.com/bar-> loads "bar" partial

No need for any adjustments in the ng-view. The '/:a' case is the only variable I have found that will acheive this.. '/:foo' does not work unless your partials are all foo1, foo2, etc... '/:a' works with any partial name.

All values fire the dynamic controller - so there is no "otherwise" but, I think it is what you're looking for in a dynamic or wildcard routing scenario..

2

As of AngularJS 1.1.3, you can now do exactly what you want using the new catch-all parameter.

https://github.com/angular/angular.js/commit/7eafbb98c64c0dc079d7d3ec589f1270b7f6fea5

From the commit:

This allows routeProvider to accept parameters that matches substrings even when they contain slashes if they are prefixed with an asterisk instead of a colon. For example, routes like edit/color/:color/largecode/*largecode will match with something like this http://appdomain.com/edit/color/brown/largecode/code/with/slashs.

I have tested it out myself (using 1.1.5) and it works great. Just keep in mind that each new URL will reload your controller, so to keep any kind of state, you may need to use a custom service.

Laurel
  • 5,965
  • 14
  • 31
  • 57
Dave
  • 3,428
  • 30
  • 28
0

Here is another solution that works good.

(function() {
    'use strict';

    angular.module('cms').config(route);
    route.$inject = ['$routeProvider'];

    function route($routeProvider) {

        $routeProvider
            .when('/:section', {
                templateUrl: buildPath
            })
            .when('/:section/:page', {
                templateUrl: buildPath
            })
            .when('/:section/:page/:task', {
                templateUrl: buildPath
            });



    }

    function buildPath(path) {

        var layout = 'layout';

        angular.forEach(path, function(value) {

            value = value.charAt(0).toUpperCase() + value.substring(1);
            layout += value;

        });

        layout += '.tpl';

        return 'client/app/layouts/' + layout;

    }

})();
kevinius
  • 4,232
  • 7
  • 48
  • 79