1

I'm building an Ionic app with nested lists of comments. I need to animate replies as child elements and retain state. Currently I'm using a simple directive with jQuery slideToggle, but this doesn't retain state (and isn't "the Angular way").

This example of slide animations by Shlomi Assaf is a great start to what I need, but it doesn't handle nested elements. I've created a nested version of his CodePen project to demonstrate the problem.

I'm not sure whether the animation function should be modified to handle nested elements, or whether my controller should call the animation on ancestor elements when the child element is animated (or after it has completed).

Assistance is appreciated. Here's the basis of the HTML using native AngularJS directives:

<button ng-click="slideToggle1=!slideToggle1">Click Me</button>
<div class="slide-toggle" ng-show="slideToggle1"> ... </div>

Here's the original animation function:

app.animation('.slide-toggle', ['$animateCss', function($animateCss) {
    var lastId = 0;
    var _cache = {};

    function getId(el) {
      var id = el[0].getAttribute("data-slide-toggle");
      if (!id) {
        id = ++lastId;
        el[0].setAttribute("data-slide-toggle", id);
      }
      return id;
    }

    function getState(id) {
      var state = _cache[id];
      if (!state) {
        state = {};
        _cache[id] = state;
      }
      return state;
    }

    function generateRunner(closing, state, animator, element, doneFn) {
      return function() {
        state.animating = true;
        state.animator = animator;
        state.doneFn = doneFn;
        animator.start().finally(function() {
          if (closing && state.doneFn === doneFn) {
            element[0].style.height = '';
          }
          state.animating = false;
          state.animator = undefined;
          state.doneFn();
        });
      }
    }

    return {
      addClass: function(element, className, doneFn) {
        if (className == 'ng-hide') {
          var state = getState(getId(element));
          var height = (state.animating && state.height) ?
            state.height : element[0].offsetHeight;

          var animator = $animateCss(element, {
            from: {
              height: height + 'px',
              opacity: 1
            },
            to: {
              height: '0px',
              opacity: 0
            }
          });
          if (animator) {
            if (state.animating) {
              state.doneFn =
                generateRunner(true,
                  state,
                  animator,
                  element,
                  doneFn);
              return state.animator.end();
            } else {
              state.height = height;
              return generateRunner(true,
                state,
                animator,
                element,
                doneFn)();
            }
          }
        }
        doneFn();
      },
      removeClass: function(element, className, doneFn) {
        if (className == 'ng-hide') {
          var state = getState(getId(element));
          var height = (state.animating && state.height) ?
            state.height : element[0].offsetHeight;

          var animator = $animateCss(element, {
            from: {
              height: '0px',
              opacity: 0
            },
            to: {
              height: height + 'px',
              opacity: 1
            }
          });

          if (animator) {
            if (state.animating) {
              state.doneFn = generateRunner(false,
                state,
                animator,
                element,
                doneFn);
              return state.animator.end();
            } else {
              state.height = height;
              return generateRunner(false,
                state,
                animator,
                element,
                doneFn)();
            }
          }
        }
        doneFn();
      }
    };
}]);
isherwood
  • 58,414
  • 16
  • 114
  • 157

3 Answers3

0

Here's what I'm using for now. It's a directive to set the parent element's height to auto so it expands or contracts with the child list height as it's toggled. If the parent list is toggled the height gets recalculated as normal for its animation.

app.directive('toggleParent', function () {
    return {
        restrict: 'C',
        compile: function (element, attr) {

            return function (scope, element) {
                element.on('click', function (event) {
                    $(this).closest('.slide-toggle').css('height', 'auto');
                });
            }
        }
    }
});

CodePen demo

I'm sure that this same functionality can be implemented with the animation instead. That's what I'd really like help with.

isherwood
  • 58,414
  • 16
  • 114
  • 157
  • 1
    While this works for a single level down, it does not work for say, 2 or more levels deep, due to only the immediate parent being set to auto. If you add a a third level here, open up all items, then collapse the very first level, and collapse the third level deep, the first level remains at its original static height. Just pointing this out for those that thought this might solve several layers of nesting, like myself. Example: http://codepen.io/anon/pen/obZYoj?editors=101 – CoryDorning Jan 05 '16 at 19:10
0

if you want to do more than one level deep, you can use this:

jQuery

app.directive('toggleParents', function () {
    return {
        compile: function (element, attr) {

            return function (scope, element) {
                element.on('click', function (event) {
                    $(this).parents('.slide-toggle').css('height', 'auto');
                });
            }
        }
    }
});

Native JS

app.directive('toggleParents', function() {
    return {
      compile: function(element, attr) {
        var parents = function(el, cls) {
              var els = [];

              while (el = el.parentElement) {
                var hasClass = el.classList.contains(cls);

                if (hasClass) {
                  els.push(el);
                }
              }
              return els;
            };
        return function(scope, element) {
          element.on('click', function(event) {

            angular.element(parents(element[0], 'slide-toggle')).css('height', 'auto');
          });
        };
      }
    };   
});
CoryDorning
  • 1,854
  • 4
  • 25
  • 36
0

You only need to add else statement here:

if (closing && state.doneFn === doneFn) {
    element[0].style.height = '';
} else {
    element[0].style.height = 'auto';
}

This way after the animation is done, height is set to auto and that solves everything on as many levels as you want.

Bojan
  • 186
  • 1
  • 5