Search code examples
htmlcssangularjscss-animationsng-animate

CSS Slide in and out animation overflow issue


I'm trying to animate a list of tiles off screen while at the same time animating a new list of tiles on screen.

I'm using Angular's ng-for to loop through an array of visible items, so there is technically only one list, but Angular's ngAnimate module keeps the evicted items alive until the animations are done.

My issue is that whenever both lists of tiles are on screen at the same time in the middle of the animation, one overflows and ends up below the other.

This is what is happening:

Image

This is what I want it to do:

enter image description here

I've tried messing with CSS transforms, absolute positioning, but I can't seem to get it right.

Here's a fiddle with a working example of this: http://jsfiddle.net/soethzfm/7/

(function() {
  var app = angular.module("animationApp", ["ngAnimate"]);

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

      $scope.message = 'Hello world';
      $scope.items = [];
      $scope.visibleItems = [];
      for (var i = 3; i < 9; i++) {
        $scope.items.push({
          name: 'Item ' + (i - 2),
          color: "#" + (i) + "0" + (i % 5) + "0" + (i % 4) + '0'
        });
      }


      $scope.firstThree = true;
      $scope.selectNextItems = function() {
        if ($scope.firstThree) {
          $scope.firstThree = false;
          $scope.visibleItems = [$scope.items[0], $scope.items[1], $scope.items[2]];
        } else {
          $scope.firstThree = true;
          $scope.visibleItems = [$scope.items[3], $scope.items[4], $scope.items[5]];
        }
      }
      $scope.selectNextItems();
    }
  ]);
})()
.item-container {
  width: 400px;
  border: 1px solid red;
  overflow: hidden;
}
.item {
  color: white;
  width: 100px;
  height: 150px;
  border: 5px solid #F3F5F6;
  display: inline-block;
  position: relative;
  cursor: pointer;
}
.item:hover {
  padding: 2px;
  border: 3px solid blue;
}
/* Animations */

.item.ng-move,
.item.ng-enter,
.item.ng-leave {
  -webkit-transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
  -moz-transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
  -ms-transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
  -o-transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
  transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
}
.item.ng-enter,
.item.ng-move {
  left: -100%;
}
.item.ng-enter.ng-enter-active,
.item.ng-move.ng-move-active {
  left: 0;
}
.item.ng-leave {
  right: 0;
}
.item.ng-leave.ng-leave-active {
  right: -100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-animate.min.js"></script>
<div ng-app="animationApp">
  <div ng-controller="FrameController as vm">

    <button ng-click="selectNextItems()">Next Page</button>

    <div class="item-container">

      <div class="item" ng-repeat="item in visibleItems" ng-style="{'background-color': item.color}">
        {{item.name}}
      </div>
    </div>
  </div>
</div>


Solution

  • The problem that you were facing is because of content being wrapped around in the parent container. The parent container's width was only 400px but each of the boxes were 100px + 2 * 5px (for border) wide. So only 3 boxes could be fit in the same line and the rest had to wrapped around to the next line. This can be seen very clearly when you make all the 6 elements visible at the same time. As you can see in the below snippet, the last 3 items wrap around to the next line.

    Note: I know nothing about AngularJS and so excuse my poor attempt at getting all six to be visible at the same time.

    (function() {
      var app = angular.module("animationApp", ["ngAnimate"]);
    
      app.controller('FrameController', ['$scope', function($scope) {
       
        $scope.message = 'Hello world';
        $scope.items = [];
        $scope.visibleItems = [];
        for (var i = 3; i < 9; i++) {
          $scope.items.push({
            name: 'Item ' + (i - 2),
            color: "#" + (i) + "0" + (i % 5) + "0" + (i % 4) + '0'
          });
        }
    
    
        $scope.firstThree = true;
        $scope.selectNextItems = function() {
          if ($scope.firstThree) {
            $scope.firstThree = false;
            $scope.visibleItems = [$scope.items[0], $scope.items[1], $scope.items[2], $scope.items[3], $scope.items[4], $scope.items[5]];
          } else {
            $scope.firstThree = true;
            $scope.visibleItems = [$scope.items[3], $scope.items[4], $scope.items[5]];
          }
        }
        $scope.selectNextItems();
      }]);
    })()
    .item-container {
      width: 400px;
      border: 1px solid red;
      overflow: hidden;
    }
    .item {
      color: white;
      width: 100px;
      height: 150px;
      border: 5px solid #F3F5F6;
      display: inline-block;
      position: relative;
      cursor: pointer;
    }
    .item:hover {
      padding: 2px;
      border: 3px solid blue;
    }
    /* Animations */
    
    .item.ng-move,
    .item.ng-enter,
    .item.ng-leave {
      transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
    }
    .item.ng-enter,
    .item.ng-move {
      left: -80%;
    }
    .item.ng-enter.ng-enter-active,
    .item.ng-move.ng-move-active {
      left: 0%;
    }
    .item.ng-leave {
      right: 0%;
    }
    .item.ng-leave.ng-leave-active {
      right: -100%;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-animate.min.js"></script>
    <div ng-app="animationApp">
      <div ng-controller="FrameController as vm">
    
        <button ng-click="selectNextItems()">Next Page</button>
    
        <div class="item-container">
    
          <div class="item" ng-repeat="item in visibleItems" ng-style="{'background-color': item.color}">
            {{item.name}}
          </div>
        </div>
      </div>
    </div>


    This issue could easily be fixed by adding a white-space: nowrap setting to the parent container. By doing this we can get all the six elements to line up on the same row and so the mismatch that is seen during the animation will go away.

    (function() {
      var app = angular.module("animationApp", ["ngAnimate"]);
    
      app.controller('FrameController', ['$scope', function($scope) {
       
        $scope.message = 'Hello world';
        $scope.items = [];
        $scope.visibleItems = [];
        for (var i = 3; i < 9; i++) {
          $scope.items.push({
            name: 'Item ' + (i - 2),
            color: "#" + (i) + "0" + (i % 5) + "0" + (i % 4) + '0'
          });
        }
    
    
        $scope.firstThree = true;
        $scope.selectNextItems = function() {
          if ($scope.firstThree) {
            $scope.firstThree = false;
            $scope.visibleItems = [$scope.items[0], $scope.items[1], $scope.items[2]];
          } else {
            $scope.firstThree = true;
            $scope.visibleItems = [$scope.items[3], $scope.items[4], $scope.items[5]];
          }
        }
        $scope.selectNextItems();
      }]);
    })()
    .item-container {
      width: 400px;
      border: 1px solid red;
      overflow: hidden;
      white-space: nowrap;
    }
    .item {
      color: white;
      width: 100px;
      height: 150px;
      border: 5px solid #F3F5F6;
      display: inline-block;
      position: relative;
      cursor: pointer;
    }
    .item:hover {
      padding: 2px;
      border: 3px solid blue;
    }
    /* Animations */
    
    .item.ng-move,
    .item.ng-enter,
    .item.ng-leave {
      transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
    }
    .item.ng-enter,
    .item.ng-move {
      left: -80%;
    }
    .item.ng-enter.ng-enter-active,
    .item.ng-move.ng-move-active {
      left: 0%;
    }
    .item.ng-leave {
      right: 0%;
    }
    .item.ng-leave.ng-leave-active {
      right: -100%;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-animate.min.js"></script>
    <div ng-app="animationApp">
      <div ng-controller="FrameController as vm">
    
        <button ng-click="selectNextItems()">Next Page</button>
    
        <div class="item-container">
    
          <div class="item" ng-repeat="item in visibleItems" ng-style="{'background-color': item.color}">
            {{item.name}}
          </div>
        </div>
      </div>
    </div>


    But as you can see in the output, the movements don't quite seem to be seamless. The reason for this is because position settings are not quiet the way they should be. As said earlier, I have very minimal idea of how Angular animations work and so I am not able to fix them fully but using the same position attributes for the enter, leave and move would be a starting point. If we modify the position from left to right or vice-versa during the course of an event, the transition effect would get nullified and it will just result in an instant jump. Another thing that you'd note with the snippet is that I've modified the positioning values also to make the effect look smooth. But these have been done based on trial and error basis. I would strongly recommend you to implement the entire thing using pure CSS at first and then port it to AngularJS.

    (function() {
      var app = angular.module("animationApp", ["ngAnimate"]);
    
      app.controller('FrameController', ['$scope', function($scope) {
       
        $scope.message = 'Hello world';
        $scope.items = [];
        $scope.visibleItems = [];
        for (var i = 3; i < 9; i++) {
          $scope.items.push({
            name: 'Item ' + (i - 2),
            color: "#" + (i) + "0" + (i % 5) + "0" + (i % 4) + '0'
          });
        }
    
    
        $scope.firstThree = true;
        $scope.selectNextItems = function() {
          if ($scope.firstThree) {
            $scope.firstThree = false;
            $scope.visibleItems = [$scope.items[0], $scope.items[1], $scope.items[2]];
          } else {
            $scope.firstThree = true;
            $scope.visibleItems = [$scope.items[3], $scope.items[4], $scope.items[5]];
          }
        }
        $scope.selectNextItems();
      }]);
    })()
    .item-container {
      width: 400px;
      border: 1px solid red;
      overflow:hidden;
      white-space: nowrap;
    }
    .item {
      color: white;
      width: 100px;
      height: 150px;
      border: 5px solid #F3F5F6;
      display: inline-block;
      position: relative;
      cursor: pointer;
    }
    .item:hover {
      padding: 2px;
      border: 3px solid blue;
    }
    
    /* Animations */
    .item.ng-move,
    .item.ng-enter,
    .item.ng-leave {
      transition: 1400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
    }
    .item.ng-enter,
    .item.ng-move {
      left: -80%;
    }
    .item.ng-enter.ng-enter-active,
    .item.ng-move.ng-move-active {
      left: 0%;
    }
    .item.ng-leave {
      left: -80%;
    }
    .item.ng-leave.ng-leave-active {
      left: 0%;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-animate.min.js"></script>
    <div ng-app="animationApp">
      <div ng-controller="FrameController as vm">
    
        <button ng-click="selectNextItems()">Next Page</button>
    
        <div class="item-container">
    
          <div class="item" ng-repeat="item in visibleItems" ng-style="{'background-color': item.color}">
            {{item.name}}
          </div>
        </div>
      </div>
    </div>