AngularJS - UI Router - programmatically add states

39,765

Solution 1

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

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

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'
});
Share:
39,765
khorvat
Author by

khorvat

Have been programming for many years mostly for the MS .NET platform, Win 32, JavaScript. • Have strong programming abilities in C#, ASP.NET, Delphi. • In-depth knowledge of numerous Web and related architectures, protocols and scripting languages. • Experienced in SQL with a working and administrative knowledge in database systems like MS SQL Server • Proficient working with XML, HTML, JavaScript, DHTML. Specialties C# programming, XHTML, JavaScript, project planning, asp.net development, Delphi programming

Updated on July 16, 2022

Comments

  • khorvat
    khorvat almost 2 years

    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,
    })
    
    1. 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.
  • Chad Robinson
    Chad Robinson over 9 years
    Thanks for the Future States contribution, Chris! I'm up-voting this one because of the value, but leaving my other answer below because that blog post also has some interesting tidbits on it.
  • Chris T
    Chris T over 9 years
    @ChadRobinson +1 for you too
  • khorvat
    khorvat over 9 years
    There are few great points in the article but for me it's kinda odd to expand the app object with stateProvider. Thanks
  • khorvat
    khorvat over 9 years
    I tested this solution and it's working great, I did have small issue because I used 'index.resource.DYNAMIC-STATE-NAME.view' and it was queuing dynamic states probably because it was identified as parent state, as soon as I changed the state name to 'index.resource.DYNAMIC-STATE-NAMEView' it was working correctly.
  • khorvat
    khorvat over 9 years
    @ChrisT now I have issues with direct url access on the lazy loaded states, is there a way to use $stateNotFound or some other event to load the states from the service before 404 redirect ?
  • Chris T
    Chris T over 9 years
    @Khorvat yes, future states does exactly that. Read the source code and search for statenotfound to learn how, or just use the future states API since I've solved a bunch of those problems for you already.
  • khorvat
    khorvat over 9 years
    Ok I give it a shoot. Thanks
  • bCliks
    bCliks almost 9 years
    @ChrisT Is it possible to remove states programmatically?
  • Leo Caseiro
    Leo Caseiro over 8 years
    So far the easiest way, don't need AMD(like Ui-Router Extras). Just need to make sure you add the provider in a new Module, otherwise won't work.
  • 32teeths
    32teeths over 7 years
    @ChrisT Your library is awesome . I am using it extensively but still not sure if I am getting the most out of it . I have used future states to build the routes in runtime after retrieving from the api . I am trying to solve a case , where ui-tabs dynamically created with the data in the controller , and I need to create states for them when the tabs are created . Just so that , hitting , /tabs/ that has tabs but nothing selected should create the states and , hitting /tabs/1 should show the first tab even though the states are not available yet . How to ?
  • Muhammad bin Yusrat
    Muhammad bin Yusrat over 7 years
    Although the states do get added, and a person is able to click certain links to navigate to different states, however, if someone enters the app's state via URL, it doesn't work with this solution. Because it probably looks at the app's config to load the default state that corresponds to the URL and is not able to find it (Because states get added later). Hence the solution needs some more thinking..
  • Chris T
    Chris T over 7 years
    The full solution (with URL mapping) is "future states" from ui-router-extras. In fact, "Future states are now part of standard ui-router 1.0.0-rc.1 and higher.
  • ChaoticTwist
    ChaoticTwist about 7 years
    Hi, I tried implementing this, but I'm getting an error saying unknown provider. How do I register the provider in config. I want to inject the provider into a controller.
  • Mattijs
    Mattijs almost 7 years
    @ChrisT I am not allowed to inject $stateRegistry in my controller. I am getting a missing provider issue. I am running UI-router 1.0.0-beta.1. DO I need to upgrade to a newer version?
  • Chris T
    Chris T almost 7 years
    Yes, beta.1 is fairly old. Latest release is 1.0.4
  • Mattijs
    Mattijs almost 7 years
    got it. 1.0.4 is doing what I expected. Thanks for that. For some reason my npm install never got me a never version of the beta.
  • nuander
    nuander about 4 years
    I tried this method but on refresh it does not work. It's not just a timing issue. I get loads of tons of digest errors