jantimon
jantimon

Reputation: 38150

javascript/css animation from dom node to dom node

ng-animate-ref allows to create a transition from one dom node to another.

ng-animate uses all the css styles like position, font-size, font-color and more from the first dom element and the second dom element and creates a css 3 animation to move the element from state a to state b.

This is exactly what I need but unfortunately I can't use Angular 1 in the current project.

Is there any reusable way to achieve the same css3 animation without moving all styles from my css files to javascript?

To illustrate the problem please see the following example. As you can see the example has no custom javascript animation code at all but only javascript code which handles the state logic switching elements from list a to b.

The animation definition is written in pure css.

Demo:
https://codepen.io/jonespen/pen/avBZpO/

Preview:
Preview of the demo

Upvotes: 33

Views: 3285

Answers (5)

Claudio Baumgartner
Claudio Baumgartner

Reputation: 142

I was inspired by all the great previous posts and turned it into a library which allows to use ng-animate without angular.

The library is called Animorph

I solved the described example with almost no custom javascript code (as the heavy parts are all in the library).

Please note that right now it doesn't sort the lists but is focused only on the animation part.

Codepen: http://codepen.io/claudiobmgrtnr/pen/NRrYaQ

Javascript:

  $(".left").on("click", "li.element", function() {
    $(this).amAppendTo('.right', {
      addClasses: ['element--green'],
      removeClasses: ['element--golden']
    });
  });
  $(".right").on("click", "li.element", function() {
    $(this).amPrependTo('.left', {
      addClasses: ['element--golden'],
      removeClasses: ['element--green']
    });
  });

SCSS:

body {
  margin: 0;
  width: 100%;
  &:after {
    content: '';
    display: table;
    width: 100%;
    clear: both;
  }
}

ul {
  list-style-type: none;
  padding: 0;
}

.element {
  width: 100px;
  height: 30px;
  line-height: 30px;
  padding: 8px;
  list-style: none;
  &--golden {
    background: goldenrod;
  }
  &--green {
    background: #bada55;
  }
  &.am-leave {
    visibility: hidden;
  }
  &.am-leave-prepare {
    visibility: hidden;
  }
  &.am-leave-active {
    height: 0;
    padding-top: 0;
    padding-bottom: 0;
  }
  &.am-enter {
    visibility: hidden;
  }
  &.am-enter-prepare {
    height: 0;
    padding-top: 0;
    padding-bottom: 0;
  }
  &.am-enter-active {
    height: 30px;
    padding-top: 8px;
    padding-bottom: 8px;
  }
  &.am-enter,
  &.am-move,
  &.am-leave {
    transition: all 300ms;
  }
}

.left {
  float: left;
}

.right {
  float: right;
}

Upvotes: 1

user3297291
user3297291

Reputation: 23382

A plain javascript solution that uses:

  • HTMLElement.getBoundingClientRect to find differences between the old and new positions of element
  • css transition to animate
  • css transform to translate

Explanation of approach:

The core idea is to have the browser only calculate/reflow the DOM once. We'll take care of the transition between the initial state and this new one ourselves.

By only transitioning (a) the GPU accelerated transform property, on (b) a small selection of elements (all <li> elements), we'll try to ensure a high frame rate.

// Store references to DOM elements we'll need:
var lists = [
  document.querySelector(".js-list0"),
  document.querySelector(".js-list1")
];
var items = Array.prototype.slice.call(document.querySelectorAll("li"));

// The function that triggers the css transitions:
var transition = (function() { 
  var keyIndex = 0,
      bboxesBefore = {},
      bboxesAfter = {},
      storeBbox = function(obj, element) {
        var key = element.getAttribute("data-key");
        if (!key) {
          element.setAttribute("data-key", "KEY_" + keyIndex++);
          return storeBbox(obj, element);
        }
        
        obj[key] = element.getBoundingClientRect();
      },
      storeBboxes = function(obj, elements) {
        return elements.forEach(storeBbox.bind(null, obj));
      };
  
  // `action` is a function that modifies the DOM from state *before* to state *after*
  // `elements` is an array of HTMLElements which we want to monitor and transition
  return function(action, elements) {
    if (!elements || !elements.length) {
      return action();
    }
    
    // Store old position
    storeBboxes(bboxesBefore, elements);
    
    // Turn off animation
    document.body.classList.toggle("animated", false);
    
    // Call action that moves stuff around
    action();
    
    // Store new position
    storeBboxes(bboxesAfter, elements);
    
    // Transform each element from its new position to its old one
    elements.forEach(function(el) {
      var key = el.getAttribute("data-key");
      var bbox = {
        before: bboxesBefore[key],
        after: bboxesAfter[key]
      };
      
      var dx = bbox.before.left - bbox.after.left;
      var dy = bbox.before.top - bbox.after.top;
      
      el.style.transform = "translate3d(" + dx + "px," + dy + "px, 0)";
    });

    // Force repaint
    elements[0].parentElement.offsetHeight;

    // Turn on CSS animations
    document.body.classList.toggle("animated", true);
   
    // Remove translation to animate to natural position
    elements.forEach(function(el) {
      el.style.transform = "";
    });
  };
}());

// Event handler & sorting/moving logic
document.querySelector("div").addEventListener("click", function(e) {
  var currentList = e.target.getAttribute("data-list");
  if (currentList) {
    var targetIndex = e.target.getAttribute("data-index");
    var nextIndex = 0;

    // Get the next list from the lists array
    var newListIndex = (+currentList + 1) % lists.length;
    var newList = lists[newListIndex];
    
    for (nextIndex; nextIndex < newList.children.length; nextIndex++) {
      if (newList.children[nextIndex].getAttribute("data-index") > targetIndex) {
        break;
      }
    }
    
    // Call the transition
    transition(function() {
      newList.insertBefore(e.target, newList.children[nextIndex]);
      e.target.setAttribute("data-list", newListIndex);
    }, items);
  }
});
div { display: flex; justify-content: space-between; }


.animated li {
  transition: transform .5s ease-in-out;
}
<h2>Example</h2>
<div>
  <ul class="js-list0">
    <li data-index="0" data-list="0">Item 1</li>
    <li data-index="3" data-list="0">Item 2</li>
    <li data-index="5" data-list="0">Item 4</li>
    <li data-index="7" data-list="0">Item 6</li>
  </ul>

  <ul class="js-list1">
    <li data-index="4" data-list="1">Item 3</li>
    <li data-index="6" data-list="1">Item 5</li>
  </ul>
</div>


Edit:

To add support for other properties you'd like to animate, follow this 4 step approach:

  1. Add the css rule to the .animated transition property:

    transition: transform .5s ease-in-out,
                background-color .5s ease-in-out;
    
  2. Store the properties computed style before you modify the DOM:

    obj[key].bgColor = window
      .getComputedStyle(element, null)
      .getPropertyValue("background-color");
    
  3. After modifying, quickly set a temporary override for the property, like we already did for the transform prop.

    el.style.backgroundColor = bbox.before.bgColor;
    
  4. After turning on the css animations, remove the temporary override to trigger the css transition:

    el.style.backgroundColor = "";
    

In action: http://codepen.io/anon/pen/pELzdr

Please note that css transitions work very well on some properties, like transform and opacity, while they might perform worse on others (like height, which usually triggers repaints). Make sure you monitor your frame rates to prevent performance issues!

Upvotes: 11

Gust van de Wal
Gust van de Wal

Reputation: 5291

Because you already use jQuery, my answer was quite easy to make

$(function(){
  var move = function(){
    var data = [0,0]
    $('.items > li').each(function(){
      var $this = $(this)
      var height = $this.outerHeight(true)
      var side = ($this.hasClass('left') ? 0 : 1)
      $this.css('top', data[side])
      data[side]+=height
    })
  }
  $(window).on('resize', function(){
    move()
  })
  $(document).on('click', '.items > li', function(){
    $(this).toggleClass('left').toggleClass('right')
    move()
  })
  move()
  $('.items').removeClass('wait')
})
.items{
  margin: 0;
  padding: 0;
  list-style: none;
}

.items > li{
  display: table;
  position: absolute;
  padding: 10px;
  color: #fff;
  cursor: pointer;
  -webkit-user-select: none;
          user-select: none;
  transition: .3s ease;
}

.items.wait > li{
  visibility: hidden;
}

.items .left{
  left: 0;
  background-color: #1ABC9C;
}

.items .right{
  left: 100%;
  transform: translateX(-100%);
  background-color: #E74C3C;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<ul class="items wait">
  <li class="left">Item 1<br />With some additional text</li>
  <li class="left">Item 2</li>
  <li class="left">Item 3</li>
  <li class="left">Item 4</li>
  <li class="right">Item 5</li>
  <li class="right">Item 6</li>
  <li class="right">Item 7</li>
  <li class="right">Item 8</li>
</ul>

The CSS makes sure that elements with class left are on the left, and the ones with class right are on the right, but because of the following two lines

left: 100%;
transform: translateX(-100%);

the left and transform value will be transitioned, but it will look as if right is set to 0.

The script recalculates everything on 3 occasions

  • document ready
  • window resize
  • when one of the items is being clicked

When you click one of the items, it will simply toggle it's class from left to right. After that, the recalculation is being done. It keeps a variable data, which keeps track of how high each column has gotten with each item that's in it, and moves every one after that that much from the top.

This script can account for elements with margin, padding, multiple lines and images, if you want.

Also, the list has a class wait, which hides all the elements until they're set for the first time. This prevents the user from seeing the items when they're not yet placed.

Hope this helps

Upvotes: 4

TTCC
TTCC

Reputation: 1055

Of course, jQuery animate can achieve it without any plugins.

Maybe there are not many lines of code, but they do have some complexity.

Here is what you want ( ps: jquery-ui only use to change color ).

$(document).ready(function() {
  var animating = false,
    durtion = 300;
  $('.items').on("click", ".items-link", function() {
    if (animating) return;
    animating = true;
    var $this = $(this),
      dir = $this.parent().hasClass("items-l") ? "r" : "l",
      color = dir == "l" ? "#0000FF" : "#F00000",
      index = $this.attr("data-index");

    var toItems = $('.items-' + dir),
      itemsLinks = toItems.find(".items-link"),
      newEle = $this.clone(true),
      nextEle = $this.next(),
      toEle;

    if (itemsLinks.length == 0) {
      toItems.append(newEle)
    } else {
      itemsLinks.each(function() {
        if ($(this).attr("data-index") > index) {
          toEle = $(this);
          return false;
        }
      });
      if (toEle) {
        toEle.before(newEle).animate({
          "marginTop": $this.outerHeight()
        }, durtion, function() {
          toEle.css("marginTop", 0);
        });
      } else {
        toEle = itemsLinks.last();
        toEle.after(newEle)
      }
    }

    nextEle && nextEle.css("marginTop", $this.outerHeight())
      .animate({
        "marginTop": 0
      }, durtion);

    var animate = newEle.position();
    animate["background-color"] = color;
    newEle.hide() && $this.css('position', 'absolute')
      .animate(animate, durtion, function() {
        newEle.show();
        $this.remove();
        animating = false;
      });
  });
});
.items {
  padding: 0;
  -webkit-transition: 300ms linear all;
  transition: 300ms linear all;
}
.items.items-l {
  float: left
}
.items.items-r {
  float: right
}
.items.items-l a {
  background: #0000FF
}
.items.items-r a {
  background: #F00000
}
.items a,
.items-link {
  color: #fff;
  padding: 10px;
  display: block;
}
.main {
  width: 100%;
}
<script type="text/javascript" src="//code.jquery.com/jquery-1.9.1.js">
</script>
<script type="text/javascript" src="//code.jquery.com/ui/1.9.2/jquery-ui.js">
</script>
<link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css">
<div class="main">
  <div class="items items-l">
    <a class="items-link" data-index="1" href="#">Item 1</a>
    <a class="items-link" data-index="2" href="#">Item 2</a>
    <a class="items-link" data-index="3" href="#">Item 3</a> 
    <a class="items-link" data-index="4" href="#">Item 4</a>
    </div>
    <div class="items items-r">
      <a href="#" class="items-link" data-index="5">Item 5</a>
      <a href="#" class="items-link" data-index="6">Item 6</a>
      <a href="#" class="items-link" data-index="7">Item 7</a>
      <a href="#" class="items-link" data-index="8">Item 8</a>
  </div>
  

Upvotes: 18

Jishnu V S
Jishnu V S

Reputation: 8409

Just copy paste this to your HTML page, this is exactly what you need

/* CSS */

#sortable {
	list-style-type: none;
	margin: 0;
	padding: 0;
	width: 500px;
}
#sortable li {
	margin: 0 125px 0 3px;
	padding: 0.4em;
	font-size: 1.4em;
	height: 18px;
	float: left;
	color: #fff;
	cursor: pointer;
}
#sortable li:nth-child(odd) {
	background: #01BC9C;
}
#sortable li:nth-child(even) {
	background: #E54A2D;
}
#sortable li span {
	position: absolute;
	margin-left: -1.3em;
}
<!-- HTML -->

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Try with this</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<link rel="stylesheet" href="/resources/demos/style.css">

<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
  $( function() {
    $( "#sortable" ).sortable();
    $( "#sortable" ).disableSelection();
  } );
  </script>
</head>
<body>
<ul id="sortable">
  <li class="ui-state-default">Item 1</li>
  <li class="ui-state-default">Item 2</li>
  <li class="ui-state-default">Item 3</li>
  <li class="ui-state-default">Item 4</li>
  <li class="ui-state-default">Item 5</li>
  <li class="ui-state-default">Item 6</li>
  <li class="ui-state-default">Item 7</li>
</ul>
</body>
</html>

Upvotes: 0

Related Questions