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

About these ads

8 thoughts on “Using the Accordion (ui.bootstrap.accordion) with AngularJS while Dynamically populating data

  1. Enrique Aparicio Castellanos

    Very well done! I very easy to understand! Congratulations!
    but I have one question. if I want to two embedded accordion, how must I do it?
    Example:

    {{item.desc}} – {{isopen}}

    Details

    Description: bla bla bla

    Details 2

    Description 2: bla bla bla

    {{subitem.desc}}

    I dont know how to manage “ChildItemController” in the js file.

    Thx!!

    Reply
    1. alexfeinberg Post author

      Use this CSS (Discovered by using F12 tools in chrome)
      .panel-default > .panel-heading

      For example:

      .panel-default > .panel-heading {
      background-color: white
      }

      Reply
    2. alexfeinberg Post author

      About an accordion inside an accordion, you’ll need a more complex data structure. Right now, in my example, I have 3 items, each with 3 subitems. You’d need a data structure with more depth, I suppose or get additional children at runtime.

      I am also wondering about the usability of such UI.

      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