41

If I have this code:

<accordion-group heading="{{group.title}}" ng-repeat="group in groups">
      {{group.content}}
</accordion-group>

Using AngularJS, angular-ui and Twitter Bootstrap, is it possible to make the accordion call some action when opened? I know I can't simply add ng-click, because that is already used after it's "compiled" to HTML for opening/collapsing of the group.

Michal
  • 15,429
  • 10
  • 73
  • 104

7 Answers7

35

Accordion groups also allow for an accordion-heading directive instead of providing it as an attribute. You can use that and then wrap your header in another tag with an ng-click.

<accordion-group ng-repeat="group in groups" heading="{{group.title}}" is-open="group.open">
  <accordion-heading>
    <span ng-click="opened(group, $index)">{{group.content}}</span>
  </accordion-heading>
</accordion-group>

Example: http://plnkr.co/edit/B3LC1X?p=preview

rastasheep
  • 10,416
  • 3
  • 27
  • 37
kjv
  • 1,057
  • 9
  • 6
  • 3
    Yes, the accordion-heading allows us to open or close panels on click, but it's not an example exposing how to open the panels from other triggers. – Elise Chant Apr 01 '14 at 07:32
  • This was the cleanest solution I could find. Thanks, have a beer on me! – krex Apr 10 '14 at 21:37
  • 5
    This is nice, the only issue is the accordion heading takes up the entire row, and the opened function will only get called when you click directly on the text – JMK May 29 '14 at 14:11
  • 1
    cool way! +1 Just beware of giving up in the first try itself by testing the click with alert or some console.log. It won't work, try it with a function present in your scope on the controller :) Reason I understand it doesn't work is because alert and console are not supported on ng-click. – Anmol Saraf Jan 15 '15 at 07:14
  • THANK YOU @AnmolSaraf : I was becoming mad trying to test it with an alert ! – hugsbrugs Jan 26 '15 at 18:53
32

Here's a solution based on pkozlowski.opensource solution.
Instead of adding a $watch on each item of the collection, you can use a dynamically defined Property. Here, you can bind the IsOpened property of the group to the is-open attribute.

<accordion-group ng-repeat="group in groups" heading="{{group.title}}" is-open="group.IsOpened">
   {{group.content}}
</accordion-group>

So, you can dynamically add the IsOpened property on each item of the collection in the controller :

$scope.groups.forEach(function(item) {
  var isOpened = false;
  Object.defineProperty(item, "IsOpened", {
    get: function() {
      return isOpened;
    },
    set: function(newValue) {
      isOpened = newValue;
      if (isOpened) {
        console.log(item); // do something...
      }
    }
  });
});

Using properties instead of watches is better for performances.

rastasheep
  • 10,416
  • 3
  • 27
  • 37
Khonsort
  • 483
  • 4
  • 6
24

There is the is-open attribute on the accordion-group which points to a bindable expression. You could watch this expression and execute some logic when a given accordion group is open. Using this technique you would change your markup to:

<accordion-group ng-repeat="group in groups" heading="{{group.title}}" is-open="group.open">
   {{group.content}}
</accordion-group>

so that you can, in the controller, prepare a desired watch expression:

$scope.$watch('groups[0].open', function(isOpen){
    if (isOpen) {
      console.log('First group was opened'); 
    }    
  });

While the above works it might be a bit cumbersome to use in practice so if you feel like this could be improved open an issue in https://github.com/angular-ui/bootstrap

pkozlowski.opensource
  • 117,202
  • 60
  • 326
  • 286
  • 2
    Could I make the watching more generic? Like - so I don't have to write `'groups[0].open'` for every row. I don't know in advance how many rows I'll have... – Michal Mar 26 '13 at 16:44
  • @Michal I'm afraid that with the current implementation is not trivial. You could setup a deep watch as in this plunk: http://plnkr.co/edit/bLnkvf?p=preview but I can't recommend this really... – pkozlowski.opensource Mar 26 '13 at 16:54
  • @pkozlowski.opensource Do we have the `is-open` option with the `accordion-heading` markup? – callmekatootie Oct 23 '13 at 15:00
  • @pkozlowski.opensource - thanks for an example. It's certainly doable, bur do you guys plan to to formally add open/close events to Accordion control? Original Bootstrap Collapsible control has ones. – vkelman Mar 11 '14 at 18:17
  • 15
    I can't believe this isn't easier to do. It's complete garbage. Of course a developer would want to watch an accordion for an open group and when it changes. It's trivial. – Cory Danielson Apr 24 '14 at 17:44
  • Is this really still the current state of this issue? This is unacceptable – Brad Orego Jul 09 '14 at 09:45
1

Here's a solution inspired by kjv's answer, which easily tracks which accordion element is open. I found difficult getting ng-click to work on the accordion heading, though surrounding the element in a <span> tag and adding the ng-click to that worked fine.

Another problem I encountered was, although the accordion elements were added to the page programmatically, the content was not. When I tried loading the content using Angular directives(ie. {{path}}) linked to a $scope variable I would be hit with undefined, hence the use of the bellow method which populates the accordion content using the ID div embedded within.

Controller:

    //initialise the open state to false
    $scope.routeDescriptors[index].openState == false

    function opened(index) 
    {
        //we need to track what state the accordion is in
        if ($scope.routeDescriptors[index].openState == true){   //close an accordion
            $scope.routeDescriptors[index].openState == false
        } else {    //open an accordion
            //if the user clicks on another accordion element
            //then the open element will be closed, so this will handle it
            if (typeof $scope.previousAccordionIndex !== 'undefined') {
                $scope.routeDescriptors[$scope.previousAccordionIndex].openState = false;
            }
            $scope.previousAccordionIndex = index;
            $scope.routeDescriptors[index].openState = true;
    }

    function populateDiv(id)
    {
        for (var x = 0; x < $scope.routeDescriptors.length; x++)
        {
            $("#_x" + x).html($scope.routeDescriptors[x]);
        }
    }

HTML:

        <div ng-hide="hideDescriptions" class="ng-hide" id="accordionrouteinfo" ng-click="populateDiv()">
            <accordion>
                <accordion-group ng-repeat="path in routeDescriptors track by $index">
                    <accordion-heading>
                        <span ng-click="opened($index)">route {{$index}}</span>
                    </accordion-heading>
                    <!-- Notice these divs are given an ID which corresponds to it's index-->
                    <div id="_x{{$index}}"></div>
                </accordion-group>
            </accordion>
        </div>
krex
  • 345
  • 3
  • 8
  • 21
1

I used an associative array to create a relationship between the opened state and the model object.

The HTML is:

  <div ng-controller="CaseController as controller">


                <accordion close-others="controller.model.closeOthers">
                    <accordion-group ng-repeat="topic in controller.model.topics track by topic.id" is-open="controller.model.opened[topic.id]">
                       <accordion-heading>
                          <h4 class="panel-title clearfix" ng-click="controller.expand(topic)">
                         <span class="pull-left">{{topic.title}}</span>
                         <span class="pull-right">Updated: {{topic.updatedDate}}</span>
                          </h4>                           
                       </accordion-heading>
                  <div class="panel-body">

                      <div class="btn-group margin-top-10">
                          <button type="button" class="btn btn-default" ng-click="controller.createComment(topic)">Add Comment<i class="fa fa-plus"></i></button>
                      </div>
                     <div class="btn-group margin-top-10">
                         <button type="button" class="btn btn-default" ng-click="controller.editTopic(topic)">Edit Topic<i class="fa fa-pencil-square-o"></i></button>
                     </div>
                      <h4>Topic Description</h4>
                      <p><strong>{{topic.description}}</strong></p>
                      <ul class="list-group">
                          <li class="list-group-item" ng-repeat="comment in topic.comments track by comment.id">
                              <h5>Comment by: {{comment.author}}<span class="pull-right">Updated: <span class="commentDate">{{comment.updatedDate}}</span> | <span class="commentTime">{{comment.updatedTime}}</span></span></h5>
                              <p>{{comment.comment}}</p>
                             <div class="btn-group">
                               <button type="button" class="btn btn-default btn-xs" ng-click="controller.editComment(topic, comment)">Edit <i class="fa fa-pencil-square-o"></i></button>
                               <button type="button" class="btn btn-default btn-xs" ng-click="controller.deleteComment(comment)">Delete <i class="fa fa-trash-o"></i></button>
                             </div>
                          </li>
                      </ul>
                  </div>

                    </accordion-group>
                </accordion>

The controller snippet is:

   self.model = {
      closeOthers : false,
      opened   : new Array(),
      topics   : undefined
   };

The 'topics' are populated on an AJAX call. Separating the 'opened' state from the model objects that are updated from the server means the state is preserved across refreshes.

I also declare the controller with ng-controller="CaseController as controller"

hunch_hunch
  • 2,283
  • 1
  • 21
  • 26
0

accordion-controller.js

MyApp.Controllers
    .controller('AccordionCtrl', ['$scope', function ($scope) {

        $scope.groups = [
            {
                title: "Dynamic Group Header - 1",
                content: "Dynamic Group Body - 1",
                open: false
            },
            {
                title: "Dynamic Group Header - 2",
                content: "Dynamic Group Body - 2",
                open: false

            },
            {
                title: "Dynamic Group Header - 3",
                content: "Dynamic Group Body - 3",
                open: false
            }
        ];

        /**
         * Open panel method
         * @param idx {Number} - Array index
         */
        $scope.openPanel = function (idx) {
            if (!$scope.groups[idx].open) {
                console.log("Opened group with idx: " + idx);
                $scope.groups[idx].open = true;
            }
        };

        /**
         * Close panel method
         * @param idx {Number} - Array index
         */
        $scope.closePanel = function (idx) {
            if ($scope.groups[idx].open) {
                console.log("Closed group with idx: " + idx);
                $scope.groups[idx].open = false;
            }
        };

    }]);

index.html

<div ng-controller="AccordionCtrl">

    <accordion>

        <accordion-group ng-repeat="group in groups" is-open="group.open">
            <button ng-click="closePanel($index)">Close me</button>
            {{group.content}}
        </accordion-group>


        <button ng-click="openPanel(0)">Set 1</button>
        <button ng-click="openPanel(1)">Set 2</button>
        <button ng-click="openPanel(2)">Set 3</button>

    </accordion>
</div>
Elise Chant
  • 5,048
  • 3
  • 29
  • 36
0

You can do it w/ an Angular directive:

html

<div uib-accordion-group is-open="property.display_detail" ng-repeat="property in properties">
  <div uib-accordion-heading ng-click="property.display_detail = ! property.display_detail">
    some heading text
  </div>
  <!-- here is the accordion body -->
  <div ng-init="i=$index">  <!-- I keep track of the index of ng-repeat -->
    <!-- and I call a custom directive -->
    <mydirective mydirective_model="properties" mydirective_index="{% verbatim ng %}{{ i }}{% endverbatim ng %}">
      here is the body
    </mydirective>
  </div>
</div>

js

app.directive("mydirective", function() {
  return {
    restrict: "EAC",  
    link: function(scope, element, attrs) {
      /* note that ng converts everything to camelCase */
      var model = attrs["mydirectiveModel"];
      var index = attrs["mydirectiveIndex"];
      var watched_name = model + "[" + index + "].display_detail"
      scope.$watch(watched_name, function(is_displayed) {
        if (is_displayed) {
          alert("you opened something");
        }
        else {
          alert("you closed something");
        }
      });
    }
  }
});

There are some idiosyncrasies about my setup there (I use Django, hence the "{% verbatim %}" tags), but the method should work.

trubliphone
  • 4,132
  • 3
  • 42
  • 66