Use CSS for anything visual. Bind event listeners to elements with a data-behavior attribute and fire your own custom events when things happen. Listen for those custom events and add/remove CSS classes to make visual changes. Got it?
I’ll be building something very similar to my task screen above, albeit a simplified version. First, some HTML:
<ul id="tasks">
<li class="task">
<h2>Task name</h2>
<p>Task description</h2>
<a href="#" class="open-task">Open</a>
<form action="/echo/json>
<input type="text" name="name">
<a href="#" class="save">Save</a>
</form>
</li>
</ul>
The form submits to a test endpoint that always returns successfully. This is just enough code to get us going. Let’s make it somewhat pretty:
body {
background-color: #eeeeee;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
.task {
overflow: auto;
background-color: #ffffff;
padding: 10px;
}
h2 {
margin: 0;
font-size: 16px;
font-weight: bold;
}
p {
margin: 0;
}
.open-task {
display: block;
float: right;
}
form {
text-align: center;
}
That gets us here:
You can follow along, use the interface and play with the code at JSFiddle:
http://jsfiddle.net/cannikin/BZNQ6/8/
The first thing we want to do is hide the form by default, so let’s add an attribute to remove it from display:
form {
text-align: center;
display: none;
}
Now we’ve established the base state of our task which is to be “closed” with no form visible. To designate our task as “open” we’re going to add a class .open to the .task container. Let’s create the style that makes that happen:
.task.open form {
display: block;
}
When something with the class .task also has the class .open any forms inside should be visible.
So how do we get that .open class added to the task? That’s a job for Javascript. Clicking the “Open” link should open the form. We need a data-behavior on the link:
<a href="#" class="open-task" data-behavior="open">Open</a>
Then we listen for the default browser click behavior by looking for elements with data-behavior=”open” and trigger our own custom event on the task container:
$('.task').on('click', '[data-behavior~=open]', function() {
$(this).closest('.task').trigger('open.task');
return false;
});
When someone clicks on an element with the behavior of open we trigger our custom event open.task on the task container itself, not the link that was clicked (closest() goes up the DOM and finds the first element that matches the parameter passed in). All of our events are collected on a single element which makes it easy to re-use this code (anywhere a task goes all the events go with it), easier to maintain in the future (we have one place to look to find all our application-specific logic) and just plain easier to think about (everything having to do with a task happens on the .task element).
return false makes sure that the link to # doesn’t actually get followed (which would make the browser jump to the top of the page).
Now we’re ready to show our form. We’ll watch for our new custom event on the task and add the class:
$('.task').on('open.task', function() {
$(this).addClass('open');
});
Ahhh, doesn’t that look nice? Try it out at JSFiddle:
http://jsfiddle.net/cannikin/BZNQ6/9/
Let’s add some functionality. We’ll allow the user to close the task back up without completing it. We need a close button:
...
<a href="#" class="open-task" data-behavior="open" href="#">Open</a>
<a class="close-task" data-behavior="close" href="#">Close</a>
...
And a style that will hide it by default:
.close-task {
display: none;
float: right;
}
The float: right; will put it in the same position the “Open” link is in currently. Remember that the task being closed is the default state, so this style means that the close button will be hidden by default. The functionality will work like so: when the task is open, show the form and show the “Close” link. When the task is closed hide the form and show only the “Open” link.
Let’s add a style that will hide the “Open” link and show the “Close” link when the task is open:
.task.open .close-task {
display: block;
}
.task.open .open-task {
display: none;
}
And the Javascript that watches for the Close button to be clicked and removes the .open class on the task:
$('.task').on('click', '[data-behavior~=close]', function() {
$(this).closest('.task').trigger('close.task');
return false;
});
$('.task').on('close.task', function() {
$(this).removeClass('open');
});
http://jsfiddle.net/cannikin/BZNQ6/10/
Okay, let’s focus on the form. Let’s pretend, for this example’s sake, that saving the form will always be successful. In that case we’ll close the form and dim the task to indicate that it’s complete.
We need a data-behavior on the “Save” link:
<a href="#" class="save" data-behavior="save">Save</a>
And we’ll attach a listener to that behavior and trigger our own event:
$('.task').on('click', '[data-behavior~=save]', function() {
$(this).closest('.task').trigger('save.task');
return false;
});
Saving a task means we’re going to make an AJAX call to the server to save the data. Listen for save.task and AJAX away:
$('.task').on('save.task', function() {
var $this = $(this);
$.post('/echo/json').success(function(data) {
$this.trigger('saved.task', data);
});
});
We fire our off our AJAX request (to a JSFiddle test endpoint) and assume it returns successfully. We listen for the success event and once again trigger our own custom event.
“Another custom event?!” you’re saying? Yes. Any time our task is in a new state we want an event to go with it. Next week our product manager requests that completing task #1 should also cause task #2 to be marked as complete. Easy: just trigger complete.task on task #2 and everything that needs to happen happens, no need to worry about appending the second task’s unique id attribute to a jQuery selector or messing with .next() and .prev() to try and select the right elements.
Notice that we’re passing a second parameter to our event trigger: $this.trigger(‘saved.task’, data); This is another under-used feature of jQuery’s event model. It allows you to pass around extra parameters to go along with the event itself.
When our form is saved we want to mark it as complete. Let’s create an event:
$('.task').on('saved.task', function(e, data) {
$(this).trigger('complete.task');
});
We’re receiving that extra data parameter that we passed in above (although we aren’t doing anything with it in our example here). Completing a task should close and dim it. We already have an event for closing a task so we can just trigger it when we hear the complete.task event:
$('.task').on('complete.task', function() {
$(this).trigger('close.task');
});
This is where these custom events start to be fun. We now have events with meaningful names that will put our task into known visual states. No more chaining a bunch of jQuery selectors together to add a class or change a background color or fade something out. Just close.task and everything happens for us.
Let’s also give the task a class of .complete so we can attach our “dimmed” visuals:
$('.task').on('complete.task', function() {
$(this).trigger('close.task');
$(this).addClass('complete');
});
Finally, we’ll add a CSS class that determines what a complete task looks like:
.task.complete {
background-color: #dddddd;
color: #999999;
}
.task.complete .open-task {
display: none;
}
We dim the background and text and also hide the Open link so it can’t be changed.
http://jsfiddle.net/cannikin/BZNQ6/
The interaction on our box isn’t very exciting—everything pops in and out of existence as we click around. Check out one last Fiddle that includes a touch of animation to make the interaction a little more pleasant. It also tightens up the JS by chaining all of the listeners together instead of re-selecting $(‘task’) over and over again (thanks ultimatedelman for the suggestion). It does sacrifice some of the code scanability for performance, however:
http://jsfiddle.net/cannikin/BZNQ6/11/
I like to organize the Javascript by keeping all of the DOM event listeners together and all of my custom event listeners together. The DOM events can usually be ignored completely when you come back to this code in the future. Reading through the custom event handlers almost tells a little story about how your UI works: “when a form is saved it’s marked as completed and closed.”
I use SASS in my own projects so my stylesheets feel much cleaner than the raw CSS used here (all the duplicate .task.open declarations, for example). Here’s what these stylesheets look like with SASS:
http://jsfiddle.net/cannikin/GnhH9/1/
There you have it: what I hope is a sensible and semantic approach to organizing your CSS and JS for maximum enjoyment and maintainability. Hopefully the name makes sense now: all of our CSS and JS is focused around events that we create that have meaning for our application.
You’re probably thinking this is a lot more code than I’m used to writing for something that seems so simple. And you’re right, it is more code. But making something flexible and maintainable will almost always include more code than just writing a quick one-off solution. The hope is that the extra typing actually makes it easier to maintain the code in the future.
Don’t underestimate the amount of time and mental overhead involved in figuring out how your own code works six months or a year later. Sometimes having more, but cleaner and well-organized code actually makes the task easier. Writing everything as a one-off will more often than not give your future self nightmares. Notice that when you view http://jsfiddle.net/cannikin/BZNQ6/6/ you can just scan down the “Custom Events” section of the JS and see all your behaviors in one neat line: open.task, close.task, save.task, saved.task, complete.task. You can find everything with one quick scan and the story of your app unfolds as you read down the page.
I would appreciate any feedback and suggestions for improvement! Leave comments here or on my blog: http://goo.gl/R0Z6O6
Let’s make the web better.