While there is some general philosophizing here, this is largely a step-by-step for building your own jquery collapsing widget. It is a writeup of my talk at Sql Saturday #324 – Baton Rouge. As such, it might seem lengthy and rambling. But the talk was over an hour so there you have it.
You might be able to tell from the title, this talk did not start out completely seriously. For the last three or four years I have talked at this event, each time trying to braindump my understanding of javascript, or underscore, or knockout, or jquery; trying to convince rooms full of people that 90% of the useful stuff in each you can learn in an hour. While I still fully believe this, this time I wanted to do something different. I decided to live-code and demonstrate how the 90%-simple-and-useful-parts of javascript that I love can be composted into something both clean and powerful.
But first, any time you want to talk javascript it’s good to understand it’s history. Here’s a series of actually factual articles. However, the general gist is this: Brendan Eich was given ten days by Netscape to invent a browser language. He based it on Scheme and Self. Marketing decided that it should look like Java, so it got some angle brackets. Then for a long time nobody bothered to update it. It’s important to keep this abbreviated history in mind as some parts of Javascript only make sense in this context.
Oh, and another note, if you have not read this book.
Do. It’s an easy read and covers most of the things you need to know. Hey, here it is for free.
So then, why this presentation at all? Well, despite the title. Javascript the Good Parts is mostly about the bad parts of Javascript. It’s about all the stuff that was cludged into it by marketing, or all the mistakes that were made as a result of the insanely hurried timeframe, or all the things that have just staled over the last fifteen years of non-evolution.
But there are good parts. In my opinion here they are:
- Use functions for everything
- Boolean Type Coercion
- JSON Notation
- Objects as hashes
- Hoisting
- Dynamic function signatures
No slides, let’s see these in action. We’re going to make a reusable collapser widget. This is what we’re going for
Click around. Nice, huh?
For simplicity’s sake we’re going to assume jquery. We’re also going to start with the html and css already written because I’ll assume you can get that knowledge elsewhere (and leave a comment if you would like another article on this). I will also purposefully avoid making this a jquery plugin – although that would be natural here, there are several things I would like to do that are easier demo’ed by a plain and simple function.
Desired API
So first, let’s consider how we need this thing to work. We need a function. That will take an element. And make it collapsible. makeCollapsible
sounds like a good function name for this, yeah?
//Pass in jQuery
makeCollapsible( $('.should-collapse').first() );
//Pass in a DOM element
makeCollapsible( document.querySelector('.should-collapse') );
//Pass in an options object that overrides defaults
makeCollapsible( '.should-collapse', {
collapsed: true //should the initial state be collapsed?
} );
Good? Great.
Ok, let’s get coding. So to start with, since makeCollapsible
will be reusable, we want it in its own module.
If you have a module system such as requirejs or browserify in place, by all means use it here, if not then you should still try to emulate it with an IIFE. Let’s create a stub function inside of an IIFE, export it to the global scope, select some elements, and invoke it on each.
(function(){
function makeCollapsible(el) {
}
window.makeCollapsible = makeCollapsible;
})()
////////////////////////////////////////////
$(function(){
$('.should-collapse').toArray()
.map(function(el){ makeCollapsible(el) })
})
That looks nice, don’t it? Note that I did the window.makeCollapsible
export at the end of the IIFE. I strongly recommend that if you need to export anything out of one of these you always want to do it at the very end. We’re doing this all in one screen, but in real life you would likely put our makeCollapsible
module in its own file.
Also note that I call toArray
on the jquery elements (called a matched set) and use the js array’s built-in .map
. This is because jquery – which preceeded the builtin map function – screwed up and inverted the parameters into the callback thereby making it harder to work with the 90% use case. Its almost entirely a matter of preference. Also I use map
instead of forEach
. Don’t worry about that. It’s cause I know what’s coming.
Working basics
Ok, fun. Now let’s get things actually working. First let’s consider the html we want to achieve. If someone later uses javascript to remove the element entirely from the page we want it to remove cleanly, that means that everything has to be contained by our element. We also want the triangle button to be visible when the element is collapsed – we therefore need it to be outside the area we will actually be collapsing. So what we’re aiming for is something like this
<p class="should-collapse collapsible">
<button class="collapsible-collapse-handle" type=button></button>
<span class="collapsible-collapse-area">
Text to collapse....
</span>
</p>
We want to wrap the element contents in a new <span class=collapsible-collapse-area>
and we want to prepend a new <button class=collapsible-collapse-handle>
. And that’s it. Our CSS will take care of the rest.
Creating this is helped tremendously by the existence of the jquery functions $.fn.wrapInner
and $.fn.prependTo
, and the jquery api for creating elements.
function makeCollapsible(el) {
var $el = $(el);
$el.addClass('collapsible');
$el.wrapInner( $('<span>', {'class': 'collapsible-collapse-area'}) );
var $collapseHandle = $('<button>', {'class': 'collapsible-collapse-handle'});
.prependTo($el)
}
If you’re not familiar with the above metnods you should read the jQuery docs, but regardless I think the gist is fairly straightforward.
Perhaps we should make it actually work, yes?
function toggle(shouldShow) {
$collapseHandle.next().toggle( !shouldShow );
$el.toggleClass( 'collapsed', !shouldShow );
}
toggle(true);
So we first select the content area which we know to be the element that follows our $collapseHandler, and use $.fn.toggle
to hide or show it. And then we $.fn.toggleClass
to mark the element collapsed or not. Go ahead, try this out, change the toggle(true)
below to toggle(false)
.
Make it work, but better
So I suppose we should make the darn thing clickable, huh? So that the collapser will…collapse? Well that can be as simple as adding
$collapseHandle.on('click', function(){
toggle(...uhhh...what should go here?)
})
hmm…seems like we need to maintain state somewhere. Lots of options here – we could test for the .collapsed
class, or store it in the element’s data. Or just create a simple variable tracking it! So following our simple just-like-you-think-it-is closure rules, we create an isOpen
variable inside of our makeCollapsible
function and voila.
We have the basics of a reusable clickable collapser.
Adding optional parameters
Not very customizable though, is it? If we want to use it throughout our application we need some options. How about we add one to set the initial state to collapsed?
$('.should-collapse').toArray().map(function(el, index){
makeCollapsible(el, {
collapsed: index > 0
})
})
So every widget after the first one should be collapsed at initialization.
Speaking of which, let’s take an options object as input
function makeCollapsible(el, options) {
var $el = $(el).addClass('collapsible');
var $collapseHandle = createStructure();
var isOpen = !options.collapsed;
...
Of course this opens up a whole bunch of intriguing opportunities.
For example, what if the user wanted to provide a custom way for our collapsible area to appear or disappear? Something like
$('.should-collapse').toArray().map(function(el, index){
makeCollapsible(el, {
collapsed: index > 0,
toggleArea: function($area, shouldOpen) {
if(shouldOpen)
$area.fadeIn();
else
$area.fadeOut();
}
})
})
Well we could do this by checking explicitly
function makeCollapsible(el, options) {
options || (options = {});
options.toggleArea || (options.toggleArea = defaultToggleArea)
...
function toggle(shouldShow) {
options.toggleArea($collapseHandle.next(), shouldShow);
$el.toggleClass( 'collapsed', !shouldShow );
isOpen = shouldShow
}
...
}
function defaultToggleArea($area, shouldShow) {
$area.toggle( shouldShow );
}
and while that’s ok, options is starting to get messy. Let’s clean that up.
Better optional parameters
(function(){
var defaultOptions = {
collapsed: false,
toggleArea: defaultToggleArea
};
function makeCollapsible(el, op) {
var options = $.extend({}, defaultOptions, op);
...
toggle(isOpen);
/////////////////////////
function toggle(shouldShow) {
options.toggleArea($collapseHandle.next(), shouldShow);
$el.toggleClass( 'collapsed', !shouldShow );
isOpen = shouldShow
}
...
}
function defaultToggleArea($area, shouldShow) {
$area.toggle( shouldShow );
}
window.makeCollapsible = makeCollapsible;
})()
A lot happened here, so lets take it step by step.
Outside the makeCollapsible
function but inside our module (so it is private) we created the defaultOptions
variable with all of our defaults set. In order to do this we needed to move defaultToggleArea
to the parent closure, but it was not using any variables except those passed to it so this is not a problem.
Next we have that weird $.extend
call. I love the $.extend
function. In fact, everyone does. It’s so awesome that every single library that I can think of implements a version of it. So what does this ubiquitously useful function do?
It merges objects.
And while we’re at it, let’s kick this party up another notch
defaultOptions object that is locally or globaly configurable
Here we’ve modified the above to add defaultOptions
directly to the makeCollapsible
function. I think most people are aware that its possible to add properties to functions but there is usually little reason to do it. In this case we decided that people might want to set defaults site-wide to achieve a consistent look and feel. We would therefore have to export defaultOptions
. While we could create another global variable, in this case it feels natural to group both the function and its defaults together using the function as a sort of namespace.
This allows our widget’s users to easily find and modify defaults.
And while we’re at it, since we’re now embracing the objects-are-just-hashes philosophy we can take the time and remove some duplciation from our fadein/out custom function. Since the only thing that is different is the name of the property we’re invoking, we can select it in a one-liner with a ternary if.
So what’s left? Our collapse/expand all buttons I supose.
Exposing public functionality
Something like this would be nice
$(function(){
makeCollapsible.defaultOptions.toggleArea = toggleAreaByFading
var collapsers = $('.should-collapse').toArray().map(makeElementACollapser);
$('.collapse-all').on('click', function(){
collapsers.map(function(c){ c.collapse() });
});
$('.expand-all').on('click', function(){
collapsers.map(function(c){ c.expand() });
});
function makeElementACollapser(el, index){
return makeCollapsible(el, {
collapsed: index > 0
})
}
function toggleAreaByFading($area, shouldShow) {
$area[shouldShow ? 'fadeIn' : 'fadeOut']()
}
})
And there you have it. We now need to have makeCollapsers return an object with methods. Surely this is a job for classes, right?
Or not. Turns out that we can just return from makeCollapsible
an object…with some methods…
$collapseHandle.on('click', function(){ toggle(!isOpen) })
toggle(isOpen);
return {
collapse: function() { toggle(false) },
expand: function() { toggle(true) }
}
Almost disappointing how easy it is, ain’t it? And yet I’ve seen many people struggle with how to achieve this for long minutes during interviews.
It’s the classical inheritance knowledge getting in the way, making you think there’s somethign you’re missing. Yet no, using simple closures and lightweight objects, we have in effect mimicked a constructor (the makeCollapsible
method itself), private methods (toggle
, createStructure
), and public methods (collapse
, expand
). All without having to bring in concepts like classes, instances, or access modifiers.
Well since that was so easy let’s go one step further and clean up the remaining code
Final cleanup
Final Cleanup and Working Demo
Notice that I changed function() { toggle(true) }
to toggle.bind(null, true)
using Function.prototype.bind to create a new function, with it’s parameter curried to true. It’s a bit of a judgement call whether this is simpler or not, but I tend to like it.
More interesting is how we cleared up the redundant code that would iterate the collapsers to call expand or contract
$('.collapse-all').on('click', invokeOnAll(collapsers, 'collapse'));
$('.expand-all').on('click', invokeOnAll(collapsers, 'expand'));
function invokeOnAll(objects, methodName) { return function invokeOnAll() {
return objects.map(function(x) { return x[methodName]() });
}}
A function that returns a function! You can learn more about this technique in Reginald Braithwaite’s Javascript Allongé, and I think it cleans up this nicely. You might note that I named the returned function invokeOnAll
as well. This is a tiny bit of defensive coding that doesn’t actually do anything. Instead it ensures that the function has a name, so that when viewing debugging stack traces I see the name rather than <anonymous function>
. It’s nice.
So there we have it, the basics of achieving a collapsible area widget. You can easily imagine adding features to it. The ability to specify how you want the handle built so you can collapse to a heading, the ability to detect when the collapsing animation has finished (if one was used), The ability to name animations (eg toggleArea: 'slide'
) rather than pass functions, and of course making it a jquery widget (though I would very much recommend here going a step further and using a jquery ui widget factory which will take care of much of this for you).
All these things are achievable and made quite simple with the basic techniques outlined above. This is really javascript the good parts – the ability to eschew more complex concepts, and still build simple, flexible, and awesome things.