7

Using angular and jquery I implemented slideToggle function. In order to only apply this function to one specific HTML element, I am using within ng-click function one parameter, my code works fine but, I want to know if exists another better way to implement directives and ng-click functions in angular:

index.html

<!DOCTYPE html>
<html ng-app="myApp" ng-controller="MainController">
<head>
    <title></title>
    <link type="text/css" rel="stylesheet" href="css/styles.css"/>
</head>
<body>
    <div>
        <input type="button" ng-click="toggle('first')" value="Toggle First">
        <input type="button" ng-click="toggle('second')" value="Toggle Second">
        <input type="button" ng-click="toggle('third')" value="Toggle third">
        <div class="div1" section="first" toggle="first" >
            <p>This is section #1</p>
        </div>
        <div class="div1" toggle="second">
            <p>This is another section #1</p>
        </div>
        <div class="div1" toggle="third">
            <p>This is 3 section #1</p>
        </div>
    </div>
</body>
<footer>
    <script src="js/jquery.min.js"></script>
    <script src="js/angular.js"></script>
    <script src="js/directives.js"></script>
</footer>
</html>

styles.css

.div1 {
    background: Brown;
    width: 200px;
    height: 200px;
    text-align: center;
}
.div1 p {
    position: relative;
    top: 50%;
}

directives.js

angular.module("myApp", []) //
    .controller('MainController', function($scope) {
        $scope.toggle = function(section) {
            console.log('<toggle function> section :' + section);
            $scope.section = section;
            $scope.$broadcast('event:toggle');
        }
    }) //
    .directive('toggle', function() {
        return function(scope, elem, attrs) {
            scope.$on('event:toggle', function() {
                if(attrs.toggle == scope.section){
                    elem.slideToggle('fast');
                }
            });
        };
    });

One concern of my own is the way that I am communicating between directive and scope:

        $scope.section = section;

and

        if(attrs.toggle == scope.section){

I will appreciate any advice for a better Angular implementation.

Thanks

GFoley83
  • 3,439
  • 2
  • 33
  • 46
Osy
  • 1,613
  • 5
  • 21
  • 35
  • If you want to keep your controller clean you could set up a service which handles your toggled containers or you can add a custom id to each broadcasted event like $broadcast('toggle'+something). – F Lekschas Mar 16 '13 at 07:26

3 Answers3

14

Plunker Demo: http://plnkr.co/edit/u69puq?p=preview

FYI, at the time of writing, the AngularJS team are adding an ngAnimate directive which will (hopefully) offer most of the animate functionality that jQuery currently offers, e.g. Slide, Fade etc, which is sorely missing at the moment IMO.

HTML

<div data-ng-controller="MainController">      
    <input type="button" value="Toggle First" ng-click="box1=!box1">
    <input type="button" value="Toggle Second" ng-click="box2=!box2">
    <input type="button" value="Toggle third" ng-click="box3=!box3">

    <div class="div1" data-slide-toggle="box1" data-slide-toggle-duration="100" >
        <p>This is section #1</p>
    </div>
    <div class="div2" data-slide-toggle="box2" data-slide-toggle-duration="2000">
        <p>This is another section #1</p>
    </div>
    <div class="div3" data-slide-toggle="box3">
        <p>This is 3 section #1</p>
    </div>
</div>

JS

var myApp = angular.module("myApp", []);

myApp.controller('MainController', function($scope) {
  $scope.box1 = $scope.box2 = $scope.box3 = true;
});

myApp.directive('slideToggle', function() {  
  return {
    restrict: 'A',      
    scope:{
      isOpen: "=slideToggle"
    },  
    link: function(scope, element, attr) {
      var slideDuration = parseInt(attr.slideToggleDuration, 10) || 200;      
      scope.$watch('isOpen', function(newVal,oldVal){
        if(newVal !== oldVal){ 
          element.stop().slideToggle(slideDuration);
        }
      });
    }
  };  
});
GFoley83
  • 3,439
  • 2
  • 33
  • 46
  • Thanks, but I think my original concern remains, you can note that the parameter for function toggle('first') indicates which element must be showed or hidden. I wanna know if the way that this parameter is communicating with directive is correct. – Osy Mar 19 '13 at 18:11
  • @Osy: See updated comments, code and Plnkr demo. Should be more inline with what you were initially after. – GFoley83 Mar 20 '13 at 20:38
  • According to AngularJs' documentation, "The only appropriate use of ngInit is for aliasing special properties of ngRepeat" : http://docs.angularjs.org/api/ng/directive/ngInit do you have any workaround here ? – Charlesthk Feb 27 '14 at 00:09
  • @GFoley83 I found your code helpful in getting my form to toggle, but don't quite understand what is going on in it. Specifically why it is referenced as data-slide-toggle but named slideToggle. I'm new to angJS but haven't seen that syntax between the 2. Also, I don't understand most of the syntax in the directive and was wondering if you could message me comments for your code block?? The only lines I fully understand are the stuff in the controller and the element.stop() line. Sorry... I've been looking for this for days and I just want to understand how this gave me the behavior I wanted. – codenaugh Oct 16 '14 at 03:29
  • @clanier9 This is just an Angular convention. Directives are declared using camel case e.g. `thisIsMyDirective`. When you add the directive to your html you have to use snake case so if we stick with `thisIsMyDirective` this could be added to an element as `this-is-my-directive`, `data-this-is-my-directive`, `x-this-is-my-directive` etc. Basically wherever you have a capital letter in your directive js, that becomes a dash followed by small letter in your html. – GFoley83 Oct 16 '14 at 03:46
  • @GFoley83 Thanks! super helpful. One last thing... what exactly are the isOpen pieces to this puzzle responsible for, and what are the newVal and oldVal? – codenaugh Oct 16 '14 at 04:21
  • @clanier9 My `slideToggle` directive is isolated so I've bound my open/closed bool variables (box1, box2, box3) from my controller to my directive through `scope.isOpen`. That's what `scope:{ isOpen: "=slideToggle" }` means on my directive; bind my local scope variable called `isOpen` to whatever variable is passed into the html attribute called `slideToggle` (which becomes `data-slide-toggle` on our html. newVal, oldVal are standard in all watch functions. Basically I'm just watch for a new value. – GFoley83 Oct 16 '14 at 04:34
  • @clanier9 Sounds like you're still getting your head around the basics. If I was you I'd go through the first two pages on egghead.io here: https://egghead.io/technologies/angularjs?order=ASC Trust me, this will help clear a lot of things up for you. – GFoley83 Oct 16 '14 at 04:35
  • @GFoley83 thanks. that makes a lot of sense. I will read through your link tomorrow. – codenaugh Oct 17 '14 at 06:16
  • Is it possible to use this with ng-repeat? Imagining solution using $index. – Justin Feb 19 '15 at 09:00
4

@GFoley83 This helped me out a lot thanks! I'm a newbie, so not sure if this is the right way to add a suggestion to help, sorry if it's not. I'm also no expert, so i'm happy for my suggestion to be improved.

I needed the item to start hidden and the ng-init didn't seem to do anything. So, I've added a fourth box with an attribute set to start hidden

Using the code @GFoley83 provided I have an additional attribute "start shown" added to the div which will be toggled.

 <input type="button" value="Toggle Fourth" ng-click="box4=!box4">
 <div class="div1" data-slide-toggle="box4" data-start-shown="false">
    <p>This is 4 section and starts hidden</p>
 </div>

The directive has also been updated and now has a check on this new attribute.If it's passed false, we'll hide - otherwise it will continue as before and start by showing the item.

     return {
        restrict: "A",
        scope: {
            isOpen: "=slideToggle"  // attribute
        },
        link: function (scope, element, attr) {
            var slideDuration = parseInt(attr.slideToggleDuration, 10) || 200;
            if (attr.startShown=="false") {
                element.hide();
            }
            scope.$watch('isOpen', function (newVal, oldVal) {
                if (newVal !== oldVal) {
                    element.stop().slideToggle(slideDuration);
                }
            });
        }
    };

Plunker updated : http://plnkr.co/edit/WB7UXK?p=preview

Community
  • 1
  • 1
Peter
  • 109
  • 6
  • Glad to be of help! I removed the `ng-init`s; as @Charlesthk pointed out, it's generally bad practice to use them to initialise variables (though when using Angular with .NET web forms, they can be handy). – GFoley83 Feb 27 '14 at 01:08
0

After trying the solution of Peter i can see that directives below slideToggle cannot acces to parent scope. This is my solution.

evoapp.directive('slideToggle', function() {
    return {
        restrict: 'A',
        scope: false ,
        link: function(scope, element, attr) {
            var slideDuration = parseInt(attr.slideToggleDuration, 10) || 200;
            var toggle = attr.toggle ;

            scope.$watch(toggle, function(newVal,oldVal){
                if(newVal !== oldVal){
                    element.stop().slideToggle(slideDuration);
                }
            });
        }
    };
});
evoratec
  • 59
  • 1
  • 3