Javascript 如果动画不';不要在取消时重新创建元素

Javascript 如果动画不';不要在取消时重新创建元素,javascript,angularjs,angular-ui-router,ng-animate,gsap,Javascript,Angularjs,Angular Ui Router,Ng Animate,Gsap,我想要一个ng ifjavascript动画,它可以为元素的离开设置动画。但是,如果在元素离开DOM之前,布尔控件ng if变回true,我希望现有元素被重用,而不是创建一个新的元素 使用一个简单的动画可以看到这个问题,该动画实际上什么都不做,只会占用时间: app.animation('.toggle', function($window) { return { leave: function(element, done) { $window.setTimeout(do

我想要一个
ng if
javascript动画,它可以为元素的离开设置动画。但是,如果在元素离开DOM之前,布尔控件
ng if
变回
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 router视图,其中在状态之间来回移动会导致同一模板多次出现在DOM中,但我希望对这个问题的回答可能会激发对更复杂路由情况的解决方案

我意识到我可以使用ng hide或ng类,但我希望在动画结束时将元素从DOM中删除。这也(希望)使这个问题的答案更类似于UI路由案例,因为UI视图的行为更像ng,如果在状态更改时将其添加/删除到DOM中


您可以在

中看到上面的示例,尝试使用ng hide而不是ng if。 发件人:

ngIf指令基于{expression}删除或重新创建DOM树的一部分。如果指定给ngIf的表达式的计算结果为假值,则该元素将从DOM中删除,否则将该元素的克隆重新插入DOM

因此,如果要防止DOM复制,请使用ng hide,因为当ng hide的值为truthy时,它只会将先前存在的DOM元素(p标记)的显示设置为display:none

回应您的编辑: 为什么不在不设置动画的情况下仅启动?


我对
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);
      };
    }
  };
});
这可以在


我怀疑在很多情况下,在enter和leave上使用相同但反向的动画会更好,但这只是上述代码的一个特例。

一种方法是根本不与ngAnimate集成,只使用一个指令来处理从DOM中添加/删除ngIf样式以及动画。代码更少,感觉更灵活,因为您不局限于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;
      });
    }
  };
});

这可以在

中看到,这是一个选项,但我想知道是否有一种合理的方法来处理转换结束时不在DOM中的元素。我对问题进行了编辑以使其更清楚。然后尝试输入项目的当前状态。例如,如果用户刚刚单击了切换,则将另一个变量设置为true,并在超时完成时将其设置为false。然后,当active已为true时,切勿调用setTimeout函数。我越来越近了吗?我不确定,我想可能需要一些代码来让它更清晰。(如果它与您上面的答案有足够的不同,那么第二个单独的答案可能是合适的)我认为
.animation
对您的plunkr没有任何影响。该行为似乎与没有
.animation
时完全相同。此外,在您的plunkr中,在我测试的所有情况下,元素总是立即消失,这不是我所追求的行为:休假时会有一个javascript动画(在我的简化示例中,这只是一个超时)
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;
      });
    }
  };
});