20

I'm trying to implement a slideDown/slideUp animation with AngularJS. I can't use CSS3's transition (unfortunately) since the height is set to auto (and I don't want to use the max-height workaround), so I'm trying to use jQuery's slideToggle method.

Given the following markup:

<ul>
    <li ng-repeat="entry in data">
        <span>{{entry.title}}</span>
        <a ng-click="clicked($event)" href>more?</a>
        <p>{{entry.description}}</p>
    </li>
</ul>

I implemented the following method in my controller:

$scope.clicked = function($event) {
    var a = jQuery($event.target);
    var p = a.next();
    p.slideToggle();
};

FIDDLE

Even if it seems to work as expected, I understood that modifying DOM shall be done exclusively within directives.

After having read AngularJS' documentation (which I find a bit light IMHO), directives are still a bit vague to me, so could anyone tell me whether the following directive respects AngularJS's best pratices or not?

.directive('testDirective', [
function() {
    return {
        restrict: 'A',
        scope: {
            entry: '=testDirective'
        },
        template: '<span>{{entry.title}}</span> ' +
                  '<a ng-click="clicked($event)" href>more?</a>' +
                  '<p>{{entry.description}}</p>',
        link: function(scope, element) {
            var p = jQuery(element.find('p'));
            scope.clicked = function($event) {
                p.slideToggle();
            };
        }
    };
}])

FIDDLE

Could it be improved? Am I allowed to use jQuery within a directive? Does it respect the separation of concerns?

Community
  • 1
  • 1
sp00m
  • 47,968
  • 31
  • 142
  • 252

2 Answers2

42

Alternatively, you can use AngularJS's $animate:

.animation('.slide', function() {
    var NG_HIDE_CLASS = 'ng-hide';
    return {
        beforeAddClass: function(element, className, done) {
            if(className === NG_HIDE_CLASS) {
                element.slideUp(done); 
            }
        },
        removeClass: function(element, className, done) {
            if(className === NG_HIDE_CLASS) {
                element.hide().slideDown(done);
            }
        }
    }
});

Use ng-hide or ng-show to show or hide the description.

    <li ng-repeat="entry in data">
        <span>{{entry.title}}</span>
        <a ng-click="expand = !expand" href="#">more?</a>
        <p class="slide" ng-show="expand">{{entry.description}}</p>
    </li>

See JSFiddle

P.S. you must include jquery and angular-animate.js

sigod
  • 3,514
  • 2
  • 21
  • 44
LostInComputer
  • 15,188
  • 4
  • 41
  • 49
  • 1
    Interesting, thank you! [Here](http://jsfiddle.net/Z2g8B/) could be another solution, what do you think about it? – sp00m Mar 26 '14 at 17:08
  • That is good too. Using ng-show is just a matter of personal preference. – LostInComputer Mar 26 '14 at 23:16
  • You also need to be using a full JQuery library for this to work. If you are just using the version of JQuery Lite that Angular includes (via `angular.element`) then `.slideDown` is not present. – CatDadCode Jun 17 '14 at 16:54
  • 1
    Cool. I got this to work thanks to the jsFiddle. How would you closed (`slideUp()`) all the other classes prior to expanding the current one (to keep all the results compact)? I've tried `jQuery(className).slideUp(done)` in various places but with no success. Also using pure jQuery (`$('.slide').slideUp();`) just seems to break things :( – Robert Johnstone Jun 19 '14 at 11:22
  • Simplest way is to store the expanded entry in the parent scope. See this [JSFiddle](http://jsfiddle.net/pJZdv/) – LostInComputer Jun 20 '14 at 01:05
  • 1
    Great answers, thanks. In case someone needs to have something expanded by default: [JSFiddle](http://jsfiddle.net/N56vW/10/) – szymonm Jun 23 '14 at 13:51
  • Sevenearths, are you saying that the slideUp() part of the process isn't animating? What's probably happening is ng-hide is taking it `display: none` and preventing the animation. Try `.animation-class.ng-hide.ng-animate { display: block !important; }` Hate going important, but jQuery is probably adding `display: none` to the element. – user608664 Jul 02 '14 at 12:02
  • @LostInComputer - This is simply amazing. But I just need one help. I implemented this on my project. What's happening is. I get all the items collapsed at once when I click on a button. Whereas in jQuery we would use the `.closest()` to find the nearest element in the container to toggle. But how do we do it Angular? – Unknown User Jul 30 '14 at 06:36
  • I'm sorry if I'd confused you. I'm not looking for hideall button. In the fiddle you've provided only `p` tag gets expanded on the click of an `a` tag. Whereas in my project when I click on `a` tag all the elements gets toggled. – Unknown User Aug 02 '14 at 03:56
  • Note that if you convert this code into Coffeescript, make sure you return `undefined` at the end of your `beforeAddClass` and `removeClass` functions. If you just blindly convert the above to CS, you'll probably inadvertently return the result of `jQuery(element).slideUp(done)` (because CS automatically returns the last line of your function, which will cause things to break in Angular. (I just wasted 15 minutes because of this!) – GMA Aug 26 '14 at 15:57
  • If anyone is having trouble with stuttering animations, remember that min-height can do that to you. Ran into this issue on Angular Material. A modified plunker of how you could resolve this here: https://jsfiddle.net/d7Lqsh59/1/ – William S Jul 14 '15 at 07:08
  • Important! The answer is not complete or fully correct, because in some cases animation is not marked completed in angularjs (temp css classes e.g. ng-hide-animate not going). That leads to problem with binded elements (with ng-model). Such elements may have falsy ng-show expression and still stay showing up. Solution: add "else {done()}" after "if{}" blocks. – Nurbol Alpysbayev Jan 31 '18 at 10:06
  • Loved it. Simple and clean. Thanks – maverickosama92 Jul 06 '21 at 17:39
8

I want to show an example of how this is done with ng-if, since I was looking for a solution for hours running into multiple completely different approaches that work with ng-show only. You may use ng-if vs ng-show if say you need directives initialized only upon becoming visible.

To use jQuery slideDown / slideUp with ng-if, you can also use ngAnimate but using different hooks: enter and leave.

app.animation('.ng-slide-down', function() {
  return {
    enter: function(element, done) {
      element.hide().slideDown()
      return function(cancelled) {};
    },
    leave: function(element, done) { 
      element.slideUp();
    },
  };
});

<div class="ng-slide-down" ng-if="isVisible">
     I will slide down upon entering the DOM.
</div>

This was surprisingly difficult to accomplish, even trying various JS based approaches and CSS only approaches. Ultimately there is a time and place for jQuery's animations.

Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245