Dynamically populating Angular ui-router states from a service

I’ve been learning AngularJS lately and was creating a sample project to showcase the power of the framework to my new team.  Our technology group supports multiple lines of business unders a larger umbrella and our users may be entitled to applications for one or more lines of business.

To model this scenario, I generated an entitlement json which looks like this:

[{
   "lob" : {
       "name" : "LOB1",
       "desc" : "First Line Of Business"
    },
    "entitled":true
  },
  {
    "lob" : {
       "name" : "LOB2",
       "desc" : "Second Line Of Business"
    },
    "entitled":true
}]

So, I’d like to have a single welcome page with a tab strip () on top where each Line-Of-Business is represented as a tab. If entitled it’ll be enabled, otherwise, i’ll appear grayed-out. That’s easy. Assuming my Entitlement Service returns the json above and sets the ‘entitlements’ variable on my $scope, I can easily generate a set of tabs thanks to the ng-repeat directive. Each tab, when clicked, would change the ‘state’ of the application causing the route to change as defined by the state function in the $stateProvider in module.configuration function.

<div style="margin: 10px" data-ng-controller="MainController">
        <div>
            <h2 style="text-align: center;">Welcome to Our Application</h2>
        </div>
        <tabset>
             <tab heading="Welcome" select="setSelectedTab()"></tab>
             <tab select="setSelectedTab(ent)"
                 active="ent.active"
                 disabled="ent.disabled"
                 ng-repeat="ent in entitlements"
                 heading="{{ent.lob.name}}">
             </tab>
        </tabset>   
 ...

To make each tab change the state, each tab will call the setSelected() function of my scope and will pass the entitlement specific to the tab to the function. This is what the function looks like:


     this.activateTab = function (tab) {
        tab.active = true;
        scope.selectedTab = tab;
     }

    scope.setSelectedTab = function (tab) {
      if (tab) {
       self.activateTab(tab);
       var currentState = rootScope.$state.current;
       var stateForLob = state.get(tab.lob.name.toLowerCase());
       if (stateForLob != currentState) {
           state.go(tab.lob.name.toLowerCase());
         }
      } else {
        state.go('home');
      }
   }

The self.activateTab(tab) simply deals with the visual representation of the tab by setting the ‘active’ property that the stylsheet reacts with to make a tab look selected. Now, the rest figures out whether a current state is same as the state associated with a click tab and if they are different, it uses the go function to change the state. This works well.

So, my manager asked me if I can dynamically generate state configuration that’s normally hard-coded in a javascript file based on the json response from the server. ‘Piece of cake’ I thought, perhaps even out-loud, as I went on a quest to inject a service into the config, which did not work. Then I tried to inject the $stateProvider into a service, but that also did not work. I could not find a single injectable entity that I could see in both the config() function and the service() function. I assumed that this was not possible. Googling did not help either, possibly because no-one wanted to to this before. What gives???!

On the way home, I watched an Angular video which showcased an interesting technique of interception of injected entities. You can do this inside the config() function. Same one that deals with $stateProvider It’s possible to setup a function which would be called every time someone asks the dependency injector to return a registered entity. Before returning such entity, you can register a function which would be called with the injected entity provided as a parameter and if you choose you can do something to it and return it or create a whole new entity which would then be injected instead. So, I thought, what if I can enhance the $rootScope object to reference a $stateProvider. Here’s what it looks like…

Update: No need to do the decorator here. You can simply attach a property to the app object that’s available inside the config method:


var app = angular.module('app', ['ui.bootstrap', 'ui.bootstrap.tpls', 'ui.router']);

app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', '$provide',
        function (stateProvider, urlRouterProvider, httpProvider, provide) {

             app.stateProvider = stateProvider;

...

The $deletege parameter is a reference to a $rootScope that’s about to be returned to whoever asks. Notice that on line 9, I am enhancing the $rootScope by adding a refernece to the stateProvider injected into my config() funciton.

Now, inside the service, I inject the root scope and then once the entitlements are loaded I enumerate through them and generate additional states, one for each entitlement. The example below assumes that all my partials are in a directory called lobs/views and they are files named named with the lowercase versions of themselves followed by .html extension. Controllers for each lob are also named with the lowercase version of the lob name followed by the word Controller. So for lob1, I would have a partial in the /lobs/views/lob1.html file and its controller would be named lob1Controller. The URL for it would be /lob1.


app.service('EntitlementService', ['$cacheFactory', '$http',
    function (cacheFactory, http) {

    //the scope variable here is the root scope

    //Visits each entitlement and adds a state for each entitlement
    this.addStatesFromEntitlements = function (entitlements) {
        entitlements.forEach(function (ent) {
            if (ent.entitled) {
                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'
                 });
            }
        });
    }

...

This approach is convoluted and I think that there may be a a better way to do this. If you find any, please let me know. Thank you.

As a matter of fact, I just noticed that refreshing the page while in a state causes the page to go to the root state, because during the refresh, not all states are available, pending service response. Oh well, back to hard-coding.

Advertisements

9 thoughts on “Dynamically populating Angular ui-router states from a service

  1. bonzaai

    Thank you SO MUCH! The idea of storing the $stateProvider in the app variabl solved a huge problem for my application.

    Nice work 😉

    Reply
  2. getstreaming

    Thanks for the posting, it helped me a lot.

    I was able to solve the problem you mentioned in the last paragraph by adding a route dynamically in a run-method of the module. In that method I check the current $state and $location and add the (non-existing) route by following some naming convention. It’s not bullet-proof but works very well.

    The drawback of that is that you add the route at 2 places – once inside the run method and once inside the service/controller as you’ve described in your blog post.

    Reply
    1. Justin Noel

      Thanks for this overview. I’ve got a similar problem and am looking for solutions. Do you have a sample of this on CodePen or Github?

      @getstreaming : Do you have a code sample of that?

      Reply
  3. Tri Hasmoro

    I found a pitfall in this assign-stateprovider-to-app-variabel model when implemented in Ionic. If we change state to the dinamically added states via a ahref/ng-href/ui-sref in ionic nav-bar, it would spit out “scrollview is null” error.

    Anyway great work, i think the problem is in ionic

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s