Tag Archives: Bootstrap

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