What's a good way to control an angular-ui accordion programmatically?

All we need is an easy explanation of the problem, so here it is.

I am using the accordion directive from http://angular-ui.github.com/bootstrap/ and I need to have more control over when the accordions open and close.

To be more precise I need a button inside the accordion-group that will close its parent accordion and open the next one (so basically mimic what clicking the next header would do if close-others was set to true).
I also need to do some validation before I can allow an accordion to be closed and the next one to be opened, and I also need to wire this up to click events on the accordion headers.

I am pretty new to angular and we’re currently rewriting an application from Backbone+JQuery to Angular. In the Backbone-version we were using Twitter Bootstrap accordions and we were opening and closing them using JQuery. While we can still keep doing this I would rather get rid of JQuery DOM manipulation completely so I am looking for a pure angular solution to this.

What I’ve tried to do in terms of validation is

<accordion-group ng-click="close($event)">

and in my controller

    event.preventDefault();
    event.stopPropagation();

This obviously did not work as the DOM element is replaced by the directive and the click-handler is never added. I’ve been going over the source code (and found a few very nice undocumented features) but I’m at a loss over where to even begin solving this specific challenge. I was considering forking angular-ui and try to add this functionality to the accordion directive but if I can achieve this without modifying the directive that would be a lot nicer.

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

There is the is-open attribute on the accordion-group which points to a bindable expression. By using this expression you can control accordion items programatically, ex.:

<div ng-controller="AccordionDemoCtrl">
  <accordion>
    <accordion-group ng-repeat="group in groups" heading="{{group.title}}" is-open="group.open">
      {{group.content}}
    </accordion-group>    
  </accordion>
  <button class="btn" ng-click="groups[0].open = !groups[0].open">Toggle first open</button>
  <button class="btn" ng-click="groups[1].open = !groups[1].open">Toggle second open</button>
</div>

and the working plunk here: http://plnkr.co/edit/DepnVH?p=preview

Method 2

For whoever the solution by @pkozlowski.opensource is not working (me for example) you could just force the component to accept the CSS that will close it (without transition that is).

The Theory: The angular directive gets expanded into standard HTML, div elements mainly, where the CSS styles give it the appearance of the accordion. The div with class .panel-collapse is the body of the accordion-group element. You can swap its second class from .in to .collapse along with a few other changes as seen below.

The Code:

$scope.toggleOpen = function(project) {

        var id = '<The ID of the accordion-group you want to close>';
        var elements = angular.element($document[0].querySelector('#'+id));
        var children = elements.children();

        for(var i = 0; i < children.length; i++) {

            var child = angular.element(children[i]);

            if(child.hasClass('panel-collapse')) {
                if(child.hasClass('in')) { // it is open
                    child.removeClass('in');
                    child.addClass('collapse');
                    child.css('height', '0px');
                } else { // it is closed
                    child.addClass('in');
                    child.removeClass('collapse');
                    child.css('height', 'auto');
                }

            }
        }
    };

As we are talking about Angular, it is very possible that you are generating the accordion through an ng-repeat tag. In this case you can also generate the id’s for the elements like:

<accordion-group ng-repeat="user in users"
                 is-disabled="user.projects.length == 0"
                 id="USER{{user._id}}">

Given a Mongoose model User, notice that the id I am giving is not user._id but has ‘USER’ appended in front. This is because Mongoose might generate id’s that start numerically and querySelector does not like that 😉 go figure!

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply