View RSS Feed

Beverleyh

SlideUp / SlideDown content without jQuery (VelocityJS)

Rating: 9 votes, 5.00 average.
The SlideUp/SlideDown methods in jQuery are said to be the "Holy Grail" of vertical animations, because they are able to (reasonably) smoothly change the height of a matched element with a sliding motion, while allowing sibling elements to move in to the empty space. If you're unsure of what I mean, take a look at this jQuery-powered Q&A/FAQs script I cobbled together, and watch how the pretty-coloured bars move to leave no gaps, when other boxes are opened and closed;


DEMO 1 - jQuery - SlideUp / SlideDown Q&A or FAQs: http://fofwebdesign.co.uk/template/_...query-faqs.htm


jQuery is a power-house, with a great many attractive qualities and uses, but it is also a beast. It's a JavaScript library that has a low learning-curve, and cuts development-time thanks to its well-tested, cross-browser compatible routines. The problem, however, is that it is huge. If you want your website to be as light-weight as possible, the extra ~100kb (minified) of jQuery bulk, is a difficult cross to bear. And if you're only including it on a web page to power one script, that just makes things all the more painful. Don't get me wrong - I love jQuery, along with all the fantastic plugins that cleverer people than me have written, but I hate the thought of having to rely on it.

So vanilla JavaScript, where possible, is now my thing; Using it to add and remove classes that, in turn, trigger CSS3 animations. But trying to recreate jQuery's SlideUp and SlideDown animations with CSS3 alone has proved to be very difficult indeed. In this blog post, author, James Steinbach, explains why CSS animation hasn't yet successfully solved the problem;

CSS Animations don't work on an unknown height
We can't transition between height:0; and height:auto; which puts a kink in animating those variable-height toggle boxes that jQuery handles so well.

Unpredictable easing and timing
You can transition between an explicit height value (but fixed heights don't play nicely with unpredictable user-added content or responsive design), or a max-height set as some large value, but this screws with easing and timing - something demonstrated perfectly here: https://www.codehive.io/boards/bUoLvRg

Transformed elements still exist in the stacking order
Content boxes that have been collapsed with a transform, still occupy space on the page so that sibling elements do not move in to fill the empty gaps - not in an animated fashion, not in any way at all.

So an animated CSS-only solution is still a no-go, which leaves us with the question "Do we really still need to use jQuery to power our sliding content/toggle boxes?". Well, to cut a long story short... No we don't.


Moving from jQuery to VelocityJS

If you haven't heard of it yet, VelocityJS is an animation engine for fast performance animations. It is as fast as CSS and delivers better performance than jQuery, particularly on mobile devices (you've probably seen janky jQuery drop-downs on mobile - Velocity is smoother). The syntax is also similar, allowing you to jump from one to the other with a minimal learning-curve. It's a third of the size of jQuery too (~33kb minified) and so doesn't have so much of a weight-impact on your web page loading time. The down-side is that you need to write your own vanilla JavaScript functions in order to trigger the animations, but that's what this blog post is about. Hopefully you will pick up the basics from this and the Silky Smooth Web Animation with VelocityJS article, and follow suit to a leaner, lighter website and codebase.


DEMO 2 - VelocityJS - SlideUp / SlideDown Q&A or FAQs: http://fofwebdesign.co.uk/template/_...ocity-faqs.htm


The first thing I did after including the Velocity code in my page, was setup some helper functions. You can find various alternatives from You Might Not Need jQuery, plainJS, and Vanilla-Helpers - the ones I used are below;
Code:
/* Check if a class exists on an element */
function hasClass(el, cls){
	if (el.className.match('(?:^|\\s)'+cls+'(?!\\S)')) { return true; } 
	}

/* Add a class if it doesn't exist on an element - replacement for jQuery .addClass() */
function addClass(el, cls){
	if (!el.className.match('(?:^|\\s)'+cls+'(?!\\S)')){ el.className += ' '+cls; } 
	}

/* Delete a class if it exists on an element - replacement for jQuery .removeClass() */
function delClass(el, cls){
	el.className = el.className.replace(new RegExp('(?:^|\\s)'+cls+'(?!\\S)'),'');
	}

/* Find the element's previous sibling element - replacement for jQuery .prev() */
function prevElementSibling(el){
	if (el.previousElementSibling){ return el.previousElementSibling; } 
	else { el = el.previousSibling; while (el.nodeType !== 1) { return el.previousSibling; } }
	}
Next, I created my alternative, Velocity-powered SlideUp() and SlideDown() functions - I called mine qaOpen() and qaClose() because I'm using them in my Q&A/FAQs script. The functions include a check for IE8 because VelocityJS animations will only work in IE8 if jQuery is present, and that just defeats the object of streamlining the code (in IE8, the boxes open but do not animate);

First up is the qaOpen() function. The line in red checks if a content box for my Q&A/FAQ is already open, and if it isn't, a Velocity slideDown animation is performed. At the same time, the ".item-open" class is applied;
Code:
function qaOpen(el){
	for(i=0; i<el.length; i++){
		if (!hasClass(prevElementSibling(el[i]), 'item-open')) {
			if (!isIE8andUnder) { Velocity(el[i], 'slideDown', { duration:500 }); }
			addClass(prevElementSibling(el[i]), 'item-open');
			}
		}
	}
As you'd expect, the qaClose() function is pretty much the same, but in reverse; A Velocity slideUp animation is performed, and the ".item-open" class is removed;
Code:
function qaClose(el){
	for(i=0; i<el.length; i++){
		if (!isIE8andUnder) { Velocity(el[i], 'slideUp', { duration: 500 }); }
		delClass(prevElementSibling(el[i]), 'item-open');
		}
	}
Now on to the jQuery replacement code. I've put the jQuery functions and vanilla JS equivalents next to each other so that you can compare between the 2.

Here's the jQuery toggle that opens and closes a Q&A/FAQ answer when the header-bar is clicked;
Code:
$('[data-goto-id]').click(function(){ // open qa-answer from same page
	var id = $(this).data('goto-id');
	if($('#' + id).is(':hidden')){ 
		$('#' + id).slideDown(500).prev().addClass('item-open'); 
		} else { 
		$('#' + id).slideUp(500).prev().removeClass('item-open');
		}
	});
And here's the vanilla JS equivalent;
Code:
for(i=0; i<qaQuestion.length; i++){ // open qa-answer from same page
	qaQuestion[i].onclick = function(){
		var el = document.querySelectorAll('#'+this.getAttribute('data-goto-id'));
		hasClass(this, 'item-open') ? qaClose(el) : qaOpen(el);
		}
	}
Here's the jQuery to close all and open all Q&As/FAQs when the "Close All" or "Open All" buttons are clicked;
Code:
$('[data-goto-close="all"]').click(function(){ // close all
	$('.qa-answer').slideUp(500).prev().removeClass('item-open');					
	});

$('[data-goto-open="all"]').click(function(){ // open all
	$('.qa-answer').slideDown(500).prev().addClass('item-open');					
	});
And the replacement vanilla JS;
Code:
document.querySelector('[data-goto-open="all"]').onclick = function(){ qaOpen(qaAnswer) } // open all

document.querySelector('[data-goto-close="all"]').onclick = function(){ qaClose(qaAnswer) } // close all
Here's the jQuery for opening all Q&As/FAQs in a group, and closing all Q&As/FAQs in a group, when the corresponding "Open Group" and "Close Group" buttons are clicked;
Code:
$('[data-goto-open]').click(function(){ // open all in group
	$('[data-goto-group="' + $(this).data('goto-open') + '"]').slideDown(500).prev().addClass('item-open');	
	});

$('[data-goto-close]').click(function(){ // close all in group
	$('[data-goto-group="' + $(this).data('goto-close') + '"]').slideUp(500).prev().removeClass('item-open');
	});
And the replacement vanilla JS;
Code:
for(i=0; i<qaGroupOpen.length; i++){ // open all in group
	qaGroupOpen[i].onclick = function(){
		var el = document.querySelectorAll('[data-goto-group="'+this.getAttribute('data-goto-open')+'"]');
		qaOpen(el);
		}
	}

for(i=0; i<qaGroupClose.length; i++){ // close all in group
	qaGroupClose[i].onclick = function(){
		var el = document.querySelectorAll('[data-goto-group="'+this.getAttribute('data-goto-close')+'"]');
		qaClose(el);
		}
	}
Lastly, the jQuery code for when actions are performed via a hashed URL from another page;
Code:
$(function(){ // open qa-answer from another page
	var hash = window.location.hash.substr(1);
	$('#' + hash).slideDown(500).prev().addClass('item-open'); // open item by id		
	$('[data-goto-group="' + hash + '"]').slideDown(500).prev().addClass('item-open'); // open group
	if(hash == 'all'){ $('.qa-answer').slideDown(500).prev().addClass('item-open'); } // open all
	});
The hash action vanilla JS;
Code:
if (qaHash) { qaOpen(document.querySelectorAll('#'+qaHash)); } // open item by id
if (qaHash) { qaOpen(document.querySelectorAll('[data-goto-group="'+qaHash+'"]')); } // open group
if (qaHash == 'all') { qaOpen(qaAnswer); } // open all
Did you follow that? I hope so, and I also hope that you'll be a little bit braver in deciding if jQuery is really needed for your projects too.

Check out the demo for the complete Velocity-powered script: http://fofwebdesign.co.uk/template/_...ocity-faqs.htm

Note that I was having a lazy moment when I embeded the VelocityJS code, rather than linking to a separate JS file. It looks big when you view the source, but don't be put off - it's *much* smaller than jQuery - you can extract it to an external file so that the workings of your web page look cleaner/smaller.

Have fun

Submit "SlideUp / SlideDown content without jQuery (VelocityJS)" to del.icio.us Submit "SlideUp / SlideDown content without jQuery (VelocityJS)" to StumbleUpon Submit "SlideUp / SlideDown content without jQuery (VelocityJS)" to Google Submit "SlideUp / SlideDown content without jQuery (VelocityJS)" to Digg

Comments

  1. molendijk's Avatar
    Thanks Beverleyh. I didn't know until now what velocity.js was.
    There's one problem with this script on my (recent) iPad Air. On the Silky Smooth page, Scroll and Reverse don't work.
  2. Beverleyh's Avatar
    The developer who wrote that tutorial may have overlooked something, but don't worry - the official documentation has other demos and they work fine.

    Scroll: http://julian.com/research/velocity/#scroll
    Reverse: http://julian.com/research/velocity/#reverse

    And they're very easy to manipulate. For example this is a combination of the "Silky Smooth" scroll demo and the official scroll demo: http://codepen.io/anon/pen/mPZZoo