强大的 ng-if 离开动画,不会在取消时重新创建元素



我想有一个基于ng-if javascript的动画,动画元素离开。但是,如果控制ng-if的布尔值在元素离开DOM之前变回true,我希望重用现有的元素,而不是创建一个新元素。

这个问题可以用一个简单的动画来说明,这个动画除了占用时间之外什么都不做:

app.animation('.toggle', function($window) {
  return {
    leave: function(element, done) {
      $window.setTimeout(done, 2000);
    }
  };
});

与HTML类似

<button ng-click="toggle = !toggle">Toggle me a lot!</button>
<p ng-if="toggle" class="toggle">Should only ever be one of these</p>

如果您可以快速连续多次对具有toggle类的元素执行ng-if条件,那么其中几个将在DOM中结束。是否有可能在任何时候DOM中最多只包含其中一个?

我的理由是,它感觉更期望(至少对我来说)已经在屏幕上的元素对状态的变化做出反应,从它在动画中的当前位置,而不是创建一个新的,好像以前的一个已经完全消失了。我的实际用例是使用Angular UI路由器视图,在状态之间来回移动会导致同一个模板多次出现在DOM中,但我希望这个问题的答案可以为更复杂的路由情况提供解决方案。

我意识到我可以使用ng-hide或ng-class,但我希望在动画结束时从DOM中删除元素。这也将(希望)使这个问题的答案更类似于UI路由情况,因为UI -view的行为更像ng-if,它在状态变化时被添加/删除到DOM中。

您可以在http://plnkr.co/edit/hvEshhqOiC31q9wSjtL1?p=preview

看到上面的例子

试试用ng-hide代替ng-if。来自:https://docs.angularjs.org/api/ng/directive/ngIf

ngIf指令基于{expression}移除或重新创建DOM树的一部分。如果赋给ngIf的表达式的值为false,则该元素将被从DOM中移除,否则该元素的克隆将被重新插入到DOM中。

所以如果你想防止DOM重复使用ng-hide,因为它只会设置之前存在的DOM元素(你的p标签)的显示为:none,当ng-hide的值为true时。

回复你的编辑:为什么不只是在没有动画的时候开火呢?http://plnkr.co/edit/WqtjvytwEfOjhwvfhgvF?p=preview

app.animation('.toggle:not(.ng-animate)', function($window) {
return {
    leave: function(element, done) {
      $window.setTimeout(done, 2000);
    }
  };
});

我破解了ng-if的源代码,想出了一种使用类似指令的方法

  • 如果离开动画被打断,则不重新创建元素
  • 在动画结束之前不会破坏离开元素的作用域,所以如果离开被打断,所有的绑定/事件等仍然有效。

下面是指令的代码,我称之为animIf。它不像ngIf那样是多元素的,我强烈怀疑在某些情况下它是危险的,因为测试是有限的。

app.directive('animIf', function($animate) {
  return {
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    link: function($scope, $element, $attr, ctrl, $transclude) {
      var latestValue, block, childScope, enterPromise, leavePromise;
      $scope.$watch($attr.animIf, function ngIfWatchAction(value) {
        latestValue = value;
        if (value) {
          if (leavePromise) {
            // Cancelling leaving animation
            // still removes the element from the DOM,
            // so we immediately put it back in
            $animate.cancel(leavePromise);
            leavePromise = null;
            enterPromise = $animate.enter(block.clone, $element.parent(), $element);
            enterPromise.then(function() {
              enterPromise = null;
            });
          } else if (!childScope) {
            // New clone to be created + injected
            $transclude(function(clone, newScope) {
              childScope = newScope;
              clone[clone.length++] = document.createComment(' end animIf: ' + $attr.animIf + ' ');
              block = {
                clone: clone
              };
              enterPromise = $animate.enter(clone, $element.parent(), $element);
              enterPromise.then(function() {
                enterPromise = null;
              });
            });
          }
        } else {
          if (enterPromise) {
            $animate.cancel(enterPromise);
            enterPromise = null;
          }
          if (block) {
            leavePromise = $animate.leave(block.clone);
            leavePromise.then(function() {
              leavePromise = null;
              if (!latestValue && childScope) {
                // Scope is only destroyed at the end of the animation
                // This is different to how ngIf works, where it is destroyed
                // at the beginning
                if (childScope) {
                  childScope.$destroy();
                  childScope = null;
                }
                block = null;
              }
            });
          }
        }
      });
    }
  };
});

为了确保它实际上使一个真正的动画成为可能,我已经集成了GSAP的TweenMax,对于一个动画,在进入到离开时是不同的,但如果它被打断,那么它会逆转到原来的位置。

app.animation('.toggle', function(TweenMax) {
  function reverseOrClear(element) {
    if (element[0]._toggleTween) {
      var tween = element[0]._toggleTween;
      tween.reversed(!tween.reversed());
    } else {
      element[0]._toggleTween = null;
    }  
  }
  function onComplete(element, done) {
     element[0]._toggleTween = null;
     done();    
  }
  return {
    enter: function(element, done) {
      function enterComplete() {
        onComplete(element, done);
      }
      // Not Using .data since data seems to be removed from element when
      // it is removed from the DOM     
      element[0]._toggleTween = element[0]._toggleTween
        || TweenMax.from(element, 1, {opacity: 0, y: 200, onComplete: enterComplete, onReverseComplete: enterComplete});
      return function() {
        reverseOrClear(element);
      };
    },
    leave: function(element, done) {
      function leaveComplete() {
        onComplete(element, done);
      }
      element[0]._toggleTween = element[0]._toggleTween
        || TweenMax.to(element, 1, {opacity: 0, y: -200, onComplete: leaveComplete, onReverseComplete: leaveComplete});
      return function() {
        reverseOrClear(element);
      };
    }
  };
});

这可以在http://plnkr.co/edit/ZkylJwkesu6sztin6ZDB?p=preview

看到

我怀疑在很多情况下,在进入和离开时使用相同但反向的动画会更好,但这只是上述代码的特殊情况。

一种方法是根本不集成ngAnimate,只使用一个指令来处理ngif风格的添加/删除DOM和动画。代码更少,感觉更灵活,因为你不局限于ngAnimate可以做什么,它更清晰,因为有更少的移动部分。

下面是一个与GSAP集成的示例

app.directive('animIf', function(TweenMax) {
  return {
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    link: function($scope, $element, $attr, ctrl, $transclude) {
      var latestValue, latestClone, childScope, tween;
      var firstTime = true;
      function onEnterComplete() {
        tween = null;
      }
      function onLeaveComplete() {
        tween = null;
        if (!latestValue) {
          childScope.$destroy();
          childScope = null;
          latestClone.remove();
          latestClone = null;
        }
      }
      $scope.$watch($attr.animIf, function ngIfWatchAction(value) {
        latestValue = value;
        if (tween) {
          tween.reversed(!tween.reversed());
        } else if (value) {
          if (!childScope) {
            $transclude(function(clone, newScope) {
              latestClone = clone;
              childScope = newScope;
              $element.after(clone);
              // Just like ngAnimate, don't animate elements initially.
              // Could be configurable if needed
              if (!firstTime) {
                // Hard coded style for this example, but could get from attributes, style sheets...
                tween = TweenMax.from(latestClone, 1, {opacity: 0, y: 200, onComplete: onEnterComplete, onReverseComplete: onLeaveComplete});
              }    
            });
          }
        } else if (childScope) {
          tween = TweenMax.to(latestClone, 1, {opacity: 0, y: -200, onComplete: onLeaveComplete, onReverseComplete: onEnterComplete});
        }
        firstTime = false;
      });
    }
  };
});

可以在http://plnkr.co/edit/IY2icRQt4dsmVVbsnduj?p=preview

查看。

最新更新