AngularJS - UI Router - programmatically add states

JavascriptAngularjsAngular Ui-RouterAngular Services

Javascript Problem Overview


Is there a way to programmatically add states to $stateProvider after module configuration, in e.g. service ?

To add more context to this question, I have a situation where I can go with two approaches:

  1. try to force the reload on the state defined in the module configuration, the problem is that state has a reloadOnSearch set to false, so when I try $state.go('state.name', {new:param}, {reload:true}); nothing happens, any ideas ?

State definition

.state('index.resource.view', {
  url: "/:resourceName/view?pageNumber&pageSize&orderBy&search",
  templateUrl: "/resourceAdministration/views/view.html",
  controller: "resourceViewCtrl",
  reloadOnSearch: false,
})

2. try to programmatically add states that I need to load from a service so routing can work properly. I'd rather go with the first option if possible.

Javascript Solutions


Solution 1 - Javascript

See -edit- for updated information

Normally states are added to the $stateProvider during the config phase. If you want to add states at runtime, you'll need to keep a reference to the $stateProvider around.

This code is untested, but should do what you want. It creates a service called runtimeStates. You can inject it into runtime code and then add states.

// config-time dependencies can be injected here at .provider() declaration
myapp.provider('runtimeStates', function runtimeStates($stateProvider) {
  // runtime dependencies for the service can be injected here, at the provider.$get() function.
  this.$get = function($q, $timeout, $state) { // for example
    return { 
      addState: function(name, state) { 
        $stateProvider.state(name, state);
      }
    }
  }
});

I've implemented some stuff called Future States in UI-Router Extras that take care of some of the corner cases for you like mapping urls to states that don't exist yet. Future States also shows how you can lazy load the source code for runtime-states. Take a look at the source code to get a feel for what is involved.

-edit- for UI-Router 1.0+

In UI-Router 1.0, states can be registered and deregistered at runtime using StateRegistry.register and StateRegistry.deregister.

To get access to the StateRegistry, inject it as $stateRegistry, or inject $uiRouter and access it via UIRouter.stateRegistry.

UI-Router 1.0 also includes Future States out of the box which handles lazy loading of state definitions, even synchronizing by URL.

Solution 2 - Javascript

Chris T nailed it! The provider is the way to go. You don't have to slap it onto the window object, saver, more protected, etc.

Cross referencing his answer with this article really helped: http://blog.xebia.com/2013/09/01/differences-between-providers-in-angularjs/#provider

The solution makes a specific modules $stateProvider during the config block accessible to other modules during their run blocks.

In my situation I'm dynamically generating dashboard states depending on a user's permissions.

During my config block, my provider is set, passing the module's stateProvider (to be accessed later).

//in dashboard.module.js

var dashboardModule = angular.module('app.modules.dashboard',[
        'app.modules.dashboard.controllers',
        'app.modules.dashboard.services',
        'app.modules.dashboard.presentations.mileage'
    ])

    .provider('$dashboardState', function($stateProvider){
        this.$get = function(PATHS, $state){
            return {
                addState: function(title, controllerAs, templatePrefix) {
                    $stateProvider.state('dashboard.' + title, {
                        url: '/' + title,
                        views: {
                            'dashboardModule@dashboard': {
                                templateUrl: PATHS.DASHBOARD + (templatePrefix ? templatePrefix + '/' : '/') + title + '/' + title + '.view.html',
                                controller: controllerAs ? controllerAs : null
                            }
                        }
                    });
                }
            }
        }
    });

That will make my Dashboard state provider available to other modules, instead of slapping it on the window.

Then in my User module's run block (controller), I can access the dashboard's state provider and inject states dynamically.

var UserControllers = angular.module('app.modules.user.controllers', [])

.controller("UserLoginController", ["$state", "$dashboardState", function($state, $dashboardState){

    $dashboardState.addState('faq', null, 'content');

    $state.go('dashboard.faq');

}]);

Solution 3 - Javascript

Yes, it is possible to do this, but because there are caching layers involved it's complex. Alex Feinberg has documented a solution on his blog here:

http://alexfeinberg.wordpress.com/2014/03/08/dynamically-populating-angular-ui-router-states-from-a-service/

The tail end of the article includes an example of creating a state by using the stateProvider:

app.stateProvider.state(ent.lob.name.toLowerCase(), {
    url: '/' + ent.lob.name.toLowerCase(),
    controller: ent.lob.name.toLowerCase() + 'Controller',
    templateUrl: 'lobs/views/' + ent.lob.name.toLowerCase() + '.html'
});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionkhorvatView Question on Stackoverflow
Solution 1 - JavascriptChris TView Answer on Stackoverflow
Solution 2 - JavascriptAlex BoisselleView Answer on Stackoverflow
Solution 3 - JavascriptChad RobinsonView Answer on Stackoverflow