masimplo
masimplo

Reputation: 3774

Single item toggle using angular directives

I have a list of "toggleable" items. When you click on the "toggleable" element, the element expands using ng-show directive. If clicked inside the item stays open, but if you click elsewhere the item contracts.

The way I implemented this is using a directive, that exposes a bound attribute isOpen. The value of isOpen is changed within the directive link function. On element click the value of isOpen is set to true and binds a close function to the $document click event. If clicked outside the element the close function is called, setting isOpen to false and unbinding the document click event.

This works fine for a single instance of this directive, but if two or more instances are introduced, the close function of every element other than the first clicked fails to set the respective isOpen attribute to false.

Here is a Plunker: http://plnkr.co/edit/ZNTRc2uL73kFZJmYOvhh?p=preview

The directive looks like this:

app.directive('myToggle', ['$document', function ($document) {
    var closeToggle = null;
    return {
        restrict: 'A',
        scope: { isOpen: '=myToggle' },
        link: link
    };

    function link(scope, element, attributes) {
        scope.$on('$locationChangeSuccess', function () { if (closeToggle) closeToggle(); });
        element.bind('click', function() {

            closeToggle = function(event) {
                if (event)
                    if (element.parent().find(event.target).length != 0 && scope.isOpen)
                        return false;

                $document.unbind('click', closeToggle);
                scope.$apply(function () {
                    scope.isOpen = false;
                });
                closeToggle = angular.noop;
            };

            if (!scope.isOpen) {
                scope.$apply(function() {
                    scope.isOpen = true;
                });
                $document.bind('click', closeToggle);
            }
        });
    };
}]);


app.directive('myToggleCard', [function () {
    // Usage:
    // 
    // Creates:
    // 
    var directive = {
        link: link,
        restrict: 'E',
        template: '<a data-my-toggle="isOpen" data-ng-show="!isOpen" href="">ClickMe</a><div data-ng-if="isOpen"> Extra Content </div>',
        replace: true,
        scope: {
        }
    };
    return directive;

    function link(scope, element, attrs) {
        scope.isOpen = false;
    }
}]);

and the html looks like this:

<div>
  <div>
      <my-toggle-card></my-toggle-card>
      <my-toggle-card></my-toggle-card>
      <my-toggle-card></my-toggle-card>
      <my-toggle-card></my-toggle-card>  
  </div>
</div>

Upvotes: 1

Views: 422

Answers (2)

koolunix
koolunix

Reputation: 1995

In your original plunker, this makes your function have a global scope, thus all elements with this directive were executing it:

closeToggle = function(event) {

If you isolate the scope of this function to just your directive, like this, it will work correctly

var closeToggle = function(event) {

Upvotes: 1

masimplo
masimplo

Reputation: 3774

Playing around with plunker and remembering an article I was reading a couple of days on scope, hoisting and function declarations it become apparent to me that using a function expression rather than a function declaration might cause the problem.

I am guessing that even though they are declared inside different instances of the same directive the closeToggle function expression gets hoisted to a higher scope and thus is shared between the directives.

Replacing

closeToggle = function(event) {

with

functions closeToggle(event) {

fixes the problem. (Plunker: http://plnkr.co/edit/N6p4eU08dfyLLSeotw5S?p=preview)

I hope I got the reason why this got fixed. I am not 100% sure I got these concepts right yet so another more complete answer is welcome.

Upvotes: 1

Related Questions