0

I'm trying to create a slideUp and slideDown function to hide and show elements with javascript animation. I'm animating the height property here but it only works one time.

The div is initially hidden. I use slideDown on it which calculates its natural height and animates it from 0px to that height, then remove the height property. To slideUp, it animates it back from the natural height to 0px, then removes height property.

The second time around, JS is not able to measure the correct height of the div. It seems like the browser remembers the last height of the div which was 0 and sticks to that.

Here's my code: https://jsfiddle.net/VeeK727/2ufnsx61/

What is happening here?

Whip
  • 1,891
  • 22
  • 43

1 Answers1

1
  1. The main problem is that you are using filling animation (fill: 'forwards'). Once the animation ended the element's animate style remain the last value and can't be overridden.

The solution is to cancel the animation.

Naive demonstration

const box = document.querySelector('#box');

let animation;

document.querySelector('#cancel').addEventListener('click', () => {
  animation.cancel();
});

document.querySelector('#setHeight').addEventListener('click', () => {
  box.style.marginTop = '100px';
});

document.querySelector('#animate').addEventListener('click', () => {
  animation = box.animate([
    {
      marginTop: '50px'
    },
  ], {
    fill: 'forwards',
    duration: 500
  });
});
#box {
  background: red;
  width: 50px;
  height: 50px;
}
<pre>
1. Click on "Set Box's margin-top 100px"
2. Click on "Animate"
3. Click on "Set Box's margin-top 100px" again  Doesn't work
4. Click on "Cancel"  Now the element is "free" and back to its place
</pre>

<button id="setHeight">Set Box's margin-top 100px</button>
<button id="animate">Animate</button>
<button id="cancel">Cancel</button>
<hr />
<div id="box"></div>
  1. Another small issue is that the element's height keeps growing because you set the height as offsetHeight which includes padding so every toggling the height increases by the padding number.

The solution is to set box-sizing: border-box.

function slideDown(el, duration = 500) {
  // momentarily show element to calculate dimensions
  Object.assign(el.style, {
    position: 'absolute',
    display: 'block',
    visibility: 'hidden',
    height: 'auto',
    overflow: 'hidden'
  });

  const elHeight = el.offsetHeight;
  el.style.removeProperty('position');
  el.style.removeProperty('visibility');
  el.style.height = '0px';
  console.log(elHeight);

  el.animate([{
      height: '0px'
    },
    {
      height: elHeight + 'px'
    }
  ], {
    duration: duration,
    fill: 'forwards'
  });
  setTimeout(function() {
    el.style.removeProperty('height');
    el.style.removeProperty('overflow');
  }, duration);
}

function slideUp(el, duration = 500) {
  el.style.overflow = 'hidden';
  console.log(el.offsetHeight);

  const animation = el.animate([{
      height: el.offsetHeight + 'px'
    },
    {
      height: '0px'
    }
  ], {
    duration: duration,
    fill: 'forwards'
  });
  setTimeout(function() { // callback not yet supported enough
    el.style.removeProperty('height');
    el.style.removeProperty('overflow');
    el.style.display = 'none';
    animation.cancel();
  }, duration);
}

const button = document.getElementById('trigger'),
  content = document.querySelector('.content');

button.addEventListener('click', function() {
  if (this.classList.contains('active')) {
    this.classList.remove('active');
    this.textContent = 'Drop It';
    slideUp(content);
  } else {
    this.classList.add('active');
    this.textContent = 'Retract It';
    slideDown(content);
  }
});
.content {
  display: none;
  position: relative;
  padding: 1rem 1.5rem;
  border: 1px solid #000;
  border-radius: 5px;
  box-sizing: border-box;
}
<button id="trigger">
  Drop It
</button>
<br><br>
<div class="content">
  <p class="no-margin"><strong>This content is revealed by a dropdown.</strong> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
    nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>

https://jsfiddle.net/moshfeu/ca1Lmx5b/6/

Some comments

document.querySelector('button').addEventListener('click', () => {
  document
    .querySelector('div')
    .animate([{
      marginTop: '50px'
    }], {
      duration: 500,
      fill: 'forwards'
    })
    .addEventListener('finish', () => {
      console.log('animation finished')
    });
});
#box {
  width: 50px;
  height: 50px;
  background: red;
}
<button>Animate</button>
<div id="box"></div>
Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
  • Thanks for the wealth of information here. Just wanted to clarify a few things. 1- I used `fill: forwards` because otherwise the animation would return the element to its original state. But with `cancel`, I see that I don't need to do that. 2- border-box is set on my website globally, I missed including it. 3- I'm using `onfinish` now – Whip Jan 17 '22 at 06:10
  • That's great! I still recommend to use transitions over animations as much as you can and in this case I don't see a reason why not to. – Mosh Feu Jan 17 '22 at 07:57
  • 1
    Its part of a library so JS will be most predictable. I'll consider that option certainly but that's for another time :) – Whip Jan 17 '22 at 09:35