Using Angular, how do I bind a click event to an element and on click, slide a sibling element down and up?
Asked Answered
D

4

18

I'm working with Angular and I'm having trouble doing something that I normally use jQuery for.

I want to bind a click event to an element and on click, slide a sibling element down and up.

This is what the jQuery would look like:

$('element').click(function() {
    $(this).siblings('element').slideToggle();
});

Using Angular I have added an ng-click attribute with a function in my markup:

<div ng-click="events.displaySibling()"></div>

And this is what my controller looks like:

app.controller('myController', ['$scope', function($scope) {

    $scope.events = {};

    $scope.events.displaySibling = function() {
        console.log('clicked');
    }

}]);

So far this is working as expected but I don't know how to accomplish the slide. Any help is very much appreciated.

Update

I have replaced what I had with a directive.

My markup now looks like this:

<div class="wrapper padding myevent"></div>

I have removed what I had in my controller and have created a new directive.

app.directive('myevent', function() {
    return {
        restrict: 'C',
        link: function(scope, element, attrs) {
            element.bind('click', function($event) {
                element.parent().children('ul').slideToggle();
            });
        }
    }
});

However, I still can't get the slide toggle to work. I don't believe slideToggle() is supported by Angular. Any suggestions?

Dyun answered 12/3, 2014 at 18:8 Comment(7)
Typically you want to put DOM manipulation in a directive as opposed to a controller.Verena
As mentioned above a directive is the right choice for all DOM manipulation. If you access the DOM from your controller you are coupling your controller to your view which means you can't change the view and be sure that the controller still works. There are a few other reasons but your DOM manipulation should live in a directive. Look into writing custom directives and this should be pretty clear, I'll give a simple example below.Edge
I think you are right about slideToggle not being included in jqLite if you include jQuery before Angular it will make use of it and you can as well. It's sometimes best to avoid this so you don't get caught up in old ways and miss out on more elegant and testable solutions but in other cases it just makes sense to include jQuery.Edge
I want to avoid using jQuery for the very reason you mentioned. I'm new to Angular and want to focus on that. Is there simply no easy way to achieve a slide toggle using Angular?Dyun
In Angular ng-animate is used to assist with adding/removing classes when certain actions occur (showing/hiding or adding/removing elements) then it's basically up to you to write the appropriate styles in your stylesheets to achieve the changes to the style and to incorporate transitions. There may be some libraries or directives already written to simplify things like jQuery does. I'm an ex Flex developer with no real jQuery experience but I find it useful in some cases for DOM traversal where jQLite is sometimes too light.Edge
Ed's answer basically shows one "angular" way of doing it where as he explains you can use ngAnimate to get the transition you want added as well (it's a bit more legwork in Angular but ultimately cleaner)Edge
Ok, thanks for clearing that up.Dyun
F
12

I'm not sure exactly on the behaviour that you're talking about, but I would encourage you to think in a slightly different way. Less jQuery, more angular.

That is, have your html something like this:

<div ng-click="visible = !visible"></div>
<div ng-show="visible">I am the sibling!</div>

You can then use the build in ng-animate to make the sibling slide - yearofmoo has an excellent overview of how $animate works.

This example is simple enough that you can put the display logic in the html, but I would otherwise encourage you to as a rule to put it into the controller, like this:

<div ng-click="toggleSibling()"></div>
<div ng-show="visible"></div>

Controller:

app.controller('SiblingExample', function($scope){

  $scope.visible = false;

  $scope.toggleSibling = function(){
    $scope.visible = !$scope.visible;
  }

});

This kind of component is also a prime candidate for a directive, which would package it all up neatly.

Filip answered 12/3, 2014 at 18:25 Comment(5)
I was going to answer the question, but @Ed has a good answer. I haven't tried ngAnimate yet, so this is what I would have done before ngAnimate was introduced. If you want to add the animation, you could use ng-class={'open':visible} and set .open to display a different state and use CSS transition to animate.Donne
Ok, so instead of using my own custom directive, I can use the built-in ngClick and ngShow directives to show / hide the sibling without any animation. I understand that the sibling with ng-show="visible" will only show if "visible" is set to true, but can you explain the expression in ng-click="visible = !visible"? What is happening here? I'll look into ng-animate. Thanks for your help!Dyun
visible = !visible is simply inverting the value of visible. Read ! as not, i.e. if visible is true, then, !visible is false. So basically, clicking the top div toggles visible between true and false.Filip
Ok. I didn't realize it was an if condition. The syntax makes me think it's simply declaring visible as false all the time.Dyun
It's not an if condition it's a javascript statement that gets executed every time you click on the element. visible and thus it's value stick around between clicks. visible's initial value is false; it's set in the call to app.controller. Without the controller and just using visible = !visible it will be falsy initially but not actually false. After the first click it will be true. Subsequent clicks will then toggle between false and true.Cyclorama
E
5
app.directive('slideMySibling', [function(){
  // Runs during compile
  return {
    // name: '',
    // priority: 1,
    // terminal: true,
    // scope: {}, // {} = isolate, true = child, false/undefined = no change
    // controller: function($scope, $element, $attrs, $transclude) {},
    // require: 'ngModel', // Array = multiple requires, ? = optional, ^ = check parent elements
    restrict: 'A', // E = Element, A = Attribute, C = Class, M = Comment
    // template: '',
    // templateUrl: '',
    // replace: true,
    // transclude: true,
    // compile: function(tElement, tAttrs, function transclude(function(scope, cloneLinkingFn){ return function linking(scope, elm, attrs){}})),
    link: function($scope, iElm, iAttrs, controller) {
      iElm.bind("click", function(){
         $(this).siblings('element').slideToggle();
      })
    }
  };
}]);

Usage would be something like

<div slide-my-sibling><button>Some thing</button></div><div>Some sibling</div>

Note all the "code" above is just for the sake of example and hasn't been actually tested.

http://plnkr.co/edit/qd2CCXR3mjWavfZ1RHgA

Here's a sample Plnkr though as mentioned in the comments this isn't an ideal setup since it still has the javascript making assumptions about the structure of the view so ideally you would do this with a few directives where one requires the other or by using events (see $emit, $broadcast, $on).

You could also have the directive create the children "programmatically" via JavaScript and circumvent the issue of not knowing what context the directive is being used in. There are a lot of potential ways to solve these issues though none of the issues will stop it from functionally working they are worth noting for the sake of re-usability, stability, testing, etc.

Edge answered 12/3, 2014 at 18:26 Comment(4)
This is no more separated from the view than the OP's example, because you haven't included a template. Imagine, for example if you had (as your usage example even shows) a siblingless element, clicking it would throw an error. I agree that a directive is the way to go, but this boilerplate code doesn't show why.Filip
You're right this isn't a fleshed out example I was simply trying to show what the structure of a directive is (just used the snippet from SublimeText) this isn't really meant to be a full on answer, I'll remove it if you prefer but thought it would be useful to see. In terms of the issue about accessing siblings your right but I think basically this can be handled a couple of ways, using extra directives that require one another or by using emit/broadcast/on to communicate between elements, there are a few options.Edge
Indeed, no need to remove your answer, I'd just suggest making it more complete by putting in a minimum-viable-code template.Filip
I don't believe Angular supports slideToggle(). Otherwise, the directive works. Thanks for your help! Any suggestions of an alternative to slideToggle()?Dyun
G
0

As per this link : https://docs.angularjs.org/api/ng/function/angular.element

AngularJs element in your code snippet represents JQuery DOM object for related element. If you want to use JQuery functions, you should use JQuery library before angular loads. For more detail, please go through above link.

Gandhi answered 20/6, 2015 at 5:44 Comment(0)
Q
-2

Best practice:

<div ng-if="view"></div>

$scope.view = true;

$scope.toggle = function(){
  $scope.view = ($scope.view) ? false : true;
}
Quaint answered 4/4, 2017 at 21:21 Comment(2)
FYI: The top answer describes this with $scope.visible = !$scope.visible;. This answer doesn't improve upon the top answer at all. Make sure new answers to old questions add more value than previous answers.Secant
Yeah, the top method is good, but not the best method for toggle !!Quaint

© 2022 - 2024 — McMap. All rights reserved.