Monthly Archives: March 2014

Using the Accordion (ui.bootstrap.accordion) with AngularJS while Dynamically populating data

I wanted to use the Accordion control in an AngularJS project for navigation.  My controller uses a service to set its scope to an object containing a list of parent-child items which I want to represent as groups and items in the accordion.

The object looks like this:


scope.items = [
                    {
                        name: "item1",
                        desc: "Item 1",
                        subitems: [
                            {
                                name: "subitem1",
                                desc: "Sub-Item 1"
                            },
                            {
                                name: "subitem2",
                                desc: "Sub-Item 2"
                            },
                            {
                                name: "subitem2",
                                desc: "Sub-Item 2"
                            }]
                    },
                    {
                        name: "item2",
                        desc: "Item 2",
                        subitems: [
                            {
                                name: "subitem1",
                                desc: "Sub-Item 1"
                            },
                            {
                                name: "subitem2",
                                desc: "Sub-Item 2"
                            },
                            {
                                name: "subitem2",
                                desc: "Sub-Item 2"
                            }]
                    },
                    {
                        name: "item3",
                        desc: "Item 3",
                        subitems: [
                            {
                                name: "subitem1",
                                desc: "Sub-Item 1"
                            },
                            {
                                name: "subitem2",
                                desc: "Sub-Item 2"
                            },
                            {
                                name: "subitem2",
                                desc: "Sub-Item 2"
                            }]
                    }
                ];

scope.default = scope.items[2];

So I want the Accordion to look like this:
Accordion1
and
Accordion2

Couple of rules.

  1. I want only one item to be open a any time.
  2. I want the default item to open when page loads. What’s a default item? Take a look at the last line of the JavaScript code above. I am setting some item to be the default by using the default property on the scope.
  3. I want the chevrons on the right side of the accordion pills to reflect the status of the state of the pill (open vs closed.)

First, let’s jump to the solution here: Plunker.

Take a look at this HTML:


    <div class="mycontainer" ng-controller="MainController">
        <div class="left-nav">
            <accordion close-others="true" >
                  <accordion-group  is-open="isopen"
                      ng-repeat="item in items" 
                      ng-controller="ItemController">
                      <accordion-heading >
                        {{item.desc}} - {{isopen}}
                          <i class="pull-right glyphicon" 
                                ng-class="{'glyphicon-chevron-down': isopen, 'glyphicon-chevron-right': !isopen}"></i>
                      </accordion-heading>

                      <div ng-repeat="subitem in item.subitems">{{subitem.desc}}</div>
                  </accordion-group>
              </accordion>
        </div>

        <div class="content">
        </div>
    </div>

Notice that the Accordion sets this tag: close-others=”true”. This tells Angular that when rendering the template for this accordion, make sure that users can only select one group at a time. This takes care of the first requirement.

To implement the second requirement, I used the is-open directive attribute on the AccordionGroup element and bound it to a property called isopen. Same property is used in the ng-class of the chevron to control whether it shows a right chevron or the down-chevron, depending on whether the AccordionGroup is open or closed. I should be able to inject some code that will be called once for each item that Angular creates during the rendering process, as creates the groups in response to the ng-repeat directive. To do this, I add the ng-controller tag on each AccordionGroup element. This way Angular will create an instance of the controller whose name specified in the attribute.

app.controller('ItemController', ['$scope', function (scope) {

                scope.$parent.isopen = (scope.$parent.default === scope.item);

                scope.$watch('isopen', function (newvalue, oldvalue, scope) {
                    scope.$parent.isopen = newvalue;
                });

            }]);

Inside the controller I set the isopen property on the parent scope to true if the default item happens to be the item that’s being processed. The scope passed into the ItemController function is a child scope created by Angular for each of the items enumerated by the ng-repeat directive.

Please note, for some reason, I have to manipulate the parent scope. I don’t understand yet why. If someone has an answer, please comment.

So, now in response to opening the AccordionGroup, the code will set the isopen property to true on the child scope of the opened group and false on all others. But because the value referenced by Angular is the isopen property on the parent scope, I must propagate it to my parent. So I watch the change in the isopen property on the child scope and once it changes I set same value on the isopen property of the parent scope. That’s the job of the callback I define in line 5 above.

You can read more about using controllers with ng-repeat directive here: http://www.bennadel.com/blog/2450-Using-ngController-With-ngRepeat-In-AngularJS.htm

Advertisements

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.