5

What is the best way of achieving this without any visible flickering and any wierdness?

The fiddle to start out: http://jsfiddle.net/35qec14b/2/

$('.element').on('click', function(e){
  this.remove();
});
.element {
  position:relative;
  width: 200px;
  margin:5px;
  padding:20px;
  cursor:pointer;
  background: rgb(150,200,250);
  transition:1s linear;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">Element 1</div>
<div class="element">Element 2<br>Second line</div>
<div class="element">Element 3</div>
<div class="element">Element 4<br>Second line</div>
<div class="element">Element 5</div>

Note: the removed element must disappear instantly in this case, as it would appear in another location and we don't want it to be visible in two places simultaneously.

Ideas so far:

  • transform:translateY for ALL elements below the removed one (probably performance intensive for large lists)
  • Animate/transform margin of the first element below, from removed element's height to 0 (leveraging chained animations? step-start?)
  • Replace the removed element with a transparent placeholder and animate it's own height to 0
Slava
  • 2,887
  • 3
  • 30
  • 39

3 Answers3

7

The best that comes to mind is to hide it, clone it for its new location (not showed here), and then animate its height

When one animate both margins, paddings and height, it becomes not so smooth, so I added an extra inner wrapper for the content so the animation only animates the height

$('.element').on('click', function(e) {
  this.style.height = $(this).height()+ 'px';
  this.classList.add('hide-me');
  (function(el) {
    setTimeout(function() {
      el.remove();
    }, 500);
  })(this);
});
.element {
  position: relative;
  width: 200px;
  overflow: hidden;
}
.element > div {
  margin: 5px;
  padding: 20px;
  background: rgb(150, 200, 250);
}

.element.hide-me {
  animation: hideme .5s forwards;
  opacity: 0;
}
@keyframes hideme {
  100% {
    height: 0;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">
  <div>
    Element 1
  </div>
</div>
<div class="element">
  <div>
    Element 2
    <br>Second line
  </div>
</div>
<div class="element">
  <div>
    Element 3
  </div>
</div>
<div class="element">
  <div>
    Element 4
    <br>Second line
  </div>
</div>
<div class="element">
  <div>
    Element 5
  </div>
</div>
Asons
  • 84,923
  • 12
  • 110
  • 165
  • Thanks LGSon. I see some kind of very fast flickering on the elements and the scrollbar. Do you think it can be reliably prevented with this approach? – Slava Jun 16 '17 at 15:08
  • No, but I think I isolated the problem by setting animations timing to higher values: https://jsfiddle.net/usdut3um/2/ – Slava Jun 16 '17 at 15:51
  • Animation is smooth in the /3 version, but the smoothness fix relies on fixed height, which cannot be fixed in my case. Nevertheless, I will try to play around with your approach. I think if I will replace the removed item with a placeholder by JS (in my case the element will be dragged out, so it can't wait for the animation) then the flickering should be solved. – Slava Jun 16 '17 at 17:59
  • @Alph.Dev I made another version, setting height dynamically, though it doesn't work and here is why: https://stackoverflow.com/questions/10619998/html5-webkit-transition-doesnt-work-if-i-set-it-right-before-setting-something – Asons Jun 16 '17 at 18:00
  • @Alph.Dev Figured it out ... to use `animation` instead of `transition` ... updated my answer – Asons Jun 16 '17 at 18:27
2

Here's the jQuery approach using .animate()

$('.element').on('click', function(e){
  var $this = $(this), $next = $this.next();
  $next.css({
    marginTop: $this.outerHeight(true)
  }).animate({
    marginTop: 5
  }, 200);
  $this.remove();
});
.element {
  position:relative;
  width: 200px;
  margin:5px;
  padding:20px;
  cursor:pointer;
  background: rgb(150,200,250);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">Element 1</div>
<div class="element">Element 2<br>Second line</div>
<div class="element">Element 3</div>
<div class="element">Element 4<br>Second line</div>
<div class="element">Element 5</div>

And here is a CSS transition approach

$('.element').on('click', function(e){
  var $this = $(this), $next = $this.next();
  $next.css({
    marginTop: $this.outerHeight(true)
  });
  setTimeout(()=>{
    $next.addClass('hide');
    setTimeout(()=>{
      $next.css({marginTop: ''}).removeClass('hide');
    }, 250)
  }, 20);
  $this.remove();
});
.element {
  position:relative;
  width: 200px;
  margin:5px;
  padding:20px;
  cursor:pointer;
  background: rgb(150,200,250);
}

.element.hide {
  transition: margin-top 0.25s linear;
  margin-top: 5px !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
(click to remove)
<div class="element">Element 1</div>
<div class="element">Element 2<br>Second line</div>
<div class="element">Element 3</div>
<div class="element">Element 4<br>Second line</div>
<div class="element">Element 5</div>
Joseph Marikle
  • 76,418
  • 17
  • 112
  • 129
  • Thanks for providing two approaches side by side. The one using `transition:margin-top` doesn't animate on my browser, can you look into it please? – Slava Jun 18 '17 at 11:07
  • @Alph.Dev What browser are you using? It could be the ecmascript 6 arrow functions or it could be that `transition` needs prefixed, but it's too difficult to tell without know what browser and version you're using. – Joseph Marikle Jun 19 '17 at 13:43
  • It works in Chrome 59, but FF 54 gives unexpected results. The wierd thing is, sometimes it animates in FF too, but most of the time there is no smooth animation. Just a jump. – Slava Jun 19 '17 at 14:55
  • @Alph.Dev Looks like it might be my poor-man's request animation frame. It's probably better to just use raq, but for compatibility, I just added a 20ms value to the first timeout. See if that works in FF54. – Joseph Marikle Jun 19 '17 at 15:44
  • That did the trick! Can you explain whats happening there? Why did it lag, and why did adding timeout help? – Slava Jun 19 '17 at 15:57
  • 1) @Alph.Dev It's there because we set the margin using an inline style and immediately change the class. If this is done at the same time, it doesn't trigger the transition because it sees the animation as already complete. We add the timeout to force a repaint before we change the class. That forces the animation to trigger. I changed it to [`requestAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame), which does the same thing more explicitly. Originally I used timeout because it has slightly better compatibility, but I don't think that's... – Joseph Marikle Jun 19 '17 at 16:05
  • 2) as much of an issue anymore. requestanimationframe has [much better compatibility now](http://caniuse.com/#search=requestanimationframe). – Joseph Marikle Jun 19 '17 at 16:06
  • @Alph.Dev I had to rollback that change. requestanimationframe isn't quite working in FF for some reason. Either way the timeout has the same effect, so it should be fine. – Joseph Marikle Jun 19 '17 at 16:11
0

Even though you are concerned that using transforms could have a negative performance, I think that the opposite is true.

Bear in mind that other solutions involve massive reflows, that probably are more performance intensive on the CPU (transforms most probably are handled by the GPU).

However, solving this with this with transforms is a little bit hard to code. Specially changing the amount of pixels that need to be moved, and injecting them into the style.

See a posible solution. I have used just JS to make it more portable.

If you are concern about performance, the result of findKeyframesRule could be assigned to a variable and reused.

document.addEventListener('DOMContentLoaded', setEvent, false);

function setEvent() {
  var elements = document.getElementsByClassName('element');

  for (var n = 0; n < elements.length; n++) {
    elements[n].addEventListener('click', remove, false);
  }
}

function remove(event) {
  var current = event.currentTarget;
  var elements = document.getElementsByClassName('move');
  for (var n = 0; n < elements.length; n++) {
    elements[n].classList.remove('move');
  }
  window.setTimeout(function() {
    remove2(current);
  }, 0);
}

function remove2(current) {
  var next = current.nextElementSibling;
  if (!next) {
    return;
  }
  var top1 = current.offsetTop;
  var top2 = next.offsetTop;
  var diff = top2 - top1;
  var newTransform = 'translateY(' + diff + 'px)';

  var rule = findKeyframesRule('move');
  var style = rule.cssRules[0].style;
  style.transform = newTransform;
  next.classList.add('move');
  current.style.height = '0px';
}

function findKeyframesRule(rule) {
  // gather all stylesheets into an array
  var ss = document.styleSheets;

  // loop through the stylesheets
  for (var i = 0; i < ss.length; i++) {

    var ss1 = ss[i];
    // loop through all the rules
    if (!ss1.cssRules) {
      alert('you are using Chrome in local files');
      return null;
    }
    for (var j = 0; j < ss1.cssRules.length; j++) {

      // find the keyframe rule whose name matches our passed parameter
      if (ss1.cssRules[j].type == window.CSSRule.KEYFRAMES_RULE && ss1.cssRules[j].name == rule)
        return ss1.cssRules[j];
    }
  }
  return null;
}
.element {
  position: relative;
  width: 200px;
  overflow: hidden;
}

.element>div {
  margin: 5px;
  padding: 20px;
  background: rgb(150, 200, 250);
}

.move,
.move~.element {
  animation: move 2s;
}

@keyframes move {
  from {
    transform: translateY(60px);
  }
  to {
    transform: translateY( 0px);
  }
}
(click to remove)
<div class="element">
  <div>
    Element 1
  </div>
</div>
<div class="element">
  <div>
    Element 2
    <br>Second line
  </div>
</div>
<div class="element">
  <div>
    Element 3
  </div>
</div>
<div class="element">
  <div>
    Element 4
    <br>Second line
  </div>
</div>
<div class="element">
  <div>
    Element 5
  </div>
</div>
vals
  • 61,425
  • 11
  • 89
  • 138