A long time ago, in a galaxy far, far away, JavaScript was a hated language. In fact, "hated" is an understatement; JavaScript was a despised language. As a result, developers generally treated it as such, only tipping their toes into the JavaScript waters when they needed to sprinkle a bit of flair into their applications. Despite the fact that there is a whole lot of good in the JavaScript language, due to widespread ignorance, few took the time to properly learn it. Instead, as some of you might remember, standard JavaScript usage involved a significant amount of copying and pasting.
"Don't bother learning what the code does, or whether it follows best practices; just paste it in!" - Worst Advice Ever
Because the rise of jQuery reignited interest in the JavaScript language, much of the information on the web is a bit sketchy.
Ironically, it turns out that much of what the development community hated had very little to do with the JavaScript language, itself. No, the real menace under the mask was the DOM, or "Document Object Model," which, especially at the time, was horribly inconsistent from browser to browser. "Sure, it may work in Firefox, but what about IE8? Okay, it may work in IE8, but what about IE7?" The list went on tirelessly!
Luckily, beginning around five years ago, the JavaScript community would see an incredible change for the better, as libraries like jQuery were introduced to the public. Not only did these libraries provide an expressive syntax that was particularly appealing to web designers, but they also managed to level the playing feel, by tucking the workarounds for the various browser quirks into its API. Trigger $.ajax
and let jQuery do the hard part. Fast-forward to today, and the JavaScript community is more vibrant than ever - largely due to the jQuery revolution.
Because the rise of jQuery reignited interest in the JavaScript language, much of the information on the web is a bit sketchy. This is less due to the writers' ignorance, and more a result of the fact that we were all learning. It takes time for best practices to emerge.
Luckily, the community has matured immensely since those days. Before we dive into some of these best practices, let's first expose some bad advice that has circulated around the web.
Don't Use jQuery
The problem with tips like this is that they take the idea of pre-optimization to an extreme.
Much like Ruby on Rails, many developers' first introduction to JavaScript was through jQuery. This lead to a common cycle: learn jQuery, fall in love, dig into vanilla JavaScript and level up.
While there's certainly nothing wrong with this cycle, it did pave the way for countless articles, which recommended that users not use jQuery in various situations, due to "performance issues." It wouldn't be uncommon to read that it's better to use vanilla for
loops, over $.each
. Or, at some point or another, you might have read that it's best practice to use document.getElementsByClassName
over jQuery's Sizzle engine, because it's faster.
The problem with tips like this is that they take the idea of pre-optimization to an extreme, and don't account for various browser inconsistencies - the things that jQuery fixed for us! Running a test and observing a savings of a few milliseconds over thousands of repetitions is not reason to abandon jQuery and its elegant syntax. Your time is much better invested tweaking parts of your application that will actually make a difference, such as the size of your images.
Multiple jQuery Objects
This second anti-pattern, again, was the result of the community (including yours truly at one point) not fully understanding what was taking place under the jQuery hood. As such, you likely came across (or wrote yourself) code, which wrapped an element in the jQuery object countless times within a function.
$('button.confirm').on('click', function() { // Do it once $('.modal').modal(); // And once more $('.modal').addClass('active'); // And again for good measure $('modal').css(...); });
While this code might, at first, appear to be harmless (and truthfully is, in the grand scheme of things), we're following the bad practice of creating multiple instances of the jQuery object. Every time that we refer to $('.modal')
, a new jQuery object is being generated. Is that smart?
Think of the DOM as a pool: every time you call
$('.modal')
, jQuery is diving into the pool, and hunting down the associated coins (or elements). When you repeatedly query the DOM for the same selector, you're essentially throwing those coins back into the water, only to jump in and find them all over again!
Always chain selectors if you intend to use them more than once. The previous code snippet can be refactored to:
$('button.confirm').on('click', function() { $('.modal') .modal() .addClass('active') .css(...); });
Alternatively, use "caching."
$('button.confirm').on('click', function() { // Do it ONLY once var modal = $('.modal'); modal.modal(); modal.addClass('active'); modal.css(...); });
With this technique, jQuery jumps into the DOM pool a total of one time, rather than three.
Selector Performance
Too much attention is paid to selector performance.
While not as ubiquitous these days, not too long ago, the web was bombarded by countless articles on optimizing selector performance in jQuery. For example, is it better to use $('div p')
or $('div').find('p')
?
Ready for the truth? It doesn't really matter. It's certainly a good idea to have a basic understanding of the way that jQuery's Sizzle engine parses your selector queries from right to left (meaning that it's better to be more specific at the end of your selector, rather than the beginning). And, of course, the more specific you can be, the better. Clearly, $('a.button')
is better for performance than $('.button')
, due to the fact that, with the former, jQuery is able to limit the search to only the anchor elements on the page, rather than all elements.
Beyond that, however, too much attention is paid to selector performance. When in doubt, put your trust in the fact that the jQuery team is comprised of the finest JavaScript developers in the industry. If there is a performance boost to be achieved in the library, they will have discovered it. And, if not them, one of the community members will submit a pull request.
With this in mind, be aware of your selectors, but don't concern yourself too much with performance implications, unless you can verbalize why doing so is necessary.
Callback Hell
jQuery has encouraged widespread use of callback functions, which can certainly provide a nice convenience. Rather than declaring a function, simply use a callback function. For example:
$('a.external').on('click', function() { // this callback function is triggered // when .external is clicked });
You've certainly written plenty of code that looks just like this; I know I have! When used sparingly, anonymous callback functions serve as helpful conveniences. The rub occurs down the line, when we enter... callback hell (trigger thunderbolt sound)!
Callback hell is when your code indents itself numerous times, as you continue nesting callback functions.
Consider the following quite common code below:
$('a.data').on('click', function() { var anchor = $(this); $(this).fadeOut(400, function() { $.ajax({ // ... success: function(data) { anchor.fadeIn(400, function() { // you've just entered callback hell }); } }); }); });
As a basic rule of thumb, the more indented your code is, the more likely that there's a code smell. Or, better yet, ask yourself, does my code look like the Mighty Ducks Flying V?
When refactoring code such as this, the key is to ask yourself, "How could this be tested?" Within this seemingly simple bit of code, an event listener is bound to a link, the element fades out, an AJAX call is being performed, upon success, the element fades back in, presumably, the resulting data will be appended somewhere. That sure is a lot to test!
Wouldn't it be better to split this code into more manageable and testable pieces? Certainly. Though the following can be optimized further, a first step to improving this code might be:
var updatePage = function(el, data) { // append fetched data to DOM }; var fetch = function(ajaxOptions) { ajaxOptions = ajaxOptions || { // url: ... // dataType: ... success: updatePage }; return $.ajax(ajaxOptions); }; $('a.data').on('click', function() { $(this).fadeOut(400, fetch); });
Even better, if you have a variety of actions to trigger, contain the relevant methods within an object.
Think about how, in a fast-food restaurant, such as McDonalds, each worker is responsible for one task. Joe does the fries, Karen registers customers, and Mike grills burgers. If all three members of the staff did everything, this would introduce a variety of maintainability problems. When changes need to be implemented, we have to meet with each person to discuss them. However, if we, for example, keep Joe exclusively focused on the fries, should we need to adjust the instructions for preparing fries, we only need to speak with Joe and no one else. You should take a similar approach to your code; each function is responsible for one task.
In the code above, the fetch
function merely triggers an AJAX call to the specified URL. The updatePage
function accepts some data, and appends it to the DOM. Now, if we want to test one of these functions to ensure that it's working as expected, for example, the updatePage
method, we can mock the data object, and send it through to the function. Easy!
Reinventing the Wheel
It's important to remember that the jQuery ecosystem has matured greatly over the last several years. Chances are, if you have a need for a particular component, then someone else has already built it. Certainly, continue building plugins to increase your understanding of the jQuery library (in fact, we'll write one in this article), but, for real-world usage, refer to any potential existing plugins before reinventing the wheel.
As an example, need a date picker for a form? Save yourself the leg-work, and instead take advantage of the community-driven - and highly tested - jQuery UI library.
Once you reference the necessary jQuery UI library and associated stylesheet, the process of adding a date picker to an input is as easy as doing:
<input id="myDateInput" type="text"> <script> $("#myDateInput").datepicker({ dateFormat: 'yy-mm-dd' }); // Demo: http://jsbin.com/ayijig/2/ </script>
Or, what about an accordion? Sure, you could write that functionality yourself, or instead, once again, take advantage of jQuery UI.
Simply create the necessary markup for your project.
<div id="accordion"> <h3><a href="#">Chapter 1</a></h3> <div><p>Some text.</p></div> <h3><a href="#">Chapter 2</a></h3> <div><p>Some text.</p></div> <h3><a href="#">Chapter 3</a></h3> <div><p>Some text.</p></div> <h3><a href="#">Section 4</a></h3> <div><p>Some text.</p></div> </div>
Then, automagically turn it into an accordion.
$(function() { $("#accordion").accordion(); });
What if you could create tabs in thirty seconds?
Create the markup:
<div id="tabs"> <ul> <li><a href="#tabs-1">About Us</a></li> <li><a href="#tabs-2">Our Mission</a></li> <li><a href="#tabs-3">Get in Touch</a></li> </ul> <div id="tabs-1"> <p>About us text.</p> </div> <div id="tabs-2"> <p>Our mission text.</p> </div> <div id="tabs-3"> <p>Get in touch text.</p> </div> </div>
And activate the plugin.
$(function() { $("#tabs").tabs(); });
Done! It doesn't even require any notable understanding of JavaScript.
Plugin Development
Let's now dig into some best practices for building jQuery plugins, which you're bound to do at some point in your development career.
We'll use a relatively simple MessageBox
plugin as the demo for our learning. Feel free to work along; in fact, please do!
The assignment: implement the necessary functionality to display dialog boxes, using the syntax, $.message('SAY TO THE USER')
. This way, for example, before deleting a record permanently, we can ask the user to confirm the action, rather than resorting to inflexible and ugly alert
boxes.
Step 1
The first step is to figure out how to "activate" $.message
. Rather than extending jQuery's prototype, for this plugin's requirements, we only need to attach a method to the jQuery namespace.
(function($) { $.message = function(text) { console.log(text); }; })(jQuery);
It's as easy as that; go ahead, try it out! When you call $.message('Here is my message')
, that string should be logged to the browser's console (Shift + Command + i, in Google Chrome).
Step 2
While there's not enough room to cover the process of testing the plugin, this is an important step that you should research. There is an amazing sense of assuredness that occurs, when refactoring tested code.
For example, when using jQuery's test suite, QUnit, we could test-drive the code from Step 1 by writing:
module('jQuery.message', { test('is available on the jQuery namespace', 1, function() { ok($.message, 'message method should exist'); }); });
The ok
function, available through QUnit, simply asserts that the first argument is a truthy value. If the message
method does not exist on the jQuery namespace, then false
will be returned, in which case the test fails.
Following the test-driven development pattern, this code would be the first step. Once you've observed the test fail, the next step would be to add the message
method, accordingly.
While, for brevity's sake, this article will not delve further into the process of test-driven development in JavaScript, you're encouraged to refer to the GitHub repo for this project to review all the tests for the plugin: https://github.com/JeffreyWay/MessageBox/blob/master/test/MessageBox_test.js
Step 3
A message
method that does nothing isn't of help to anybody! Let's take the provided message, and display it to the user. However, rather than embedding a huge glob of code into the $.message
method, we'll instead simply use the function to instantiate and initialize a Message
object.
(function($) { "use strict"; var Message = { initialize: function(text) { this.text = text; return this; } }; $.message = function(text) { // Needs polyfill for IE8-- return Object.create(Message).initialize(text); }; })(jQuery);
Not only does this approach, again, make the Message
object more testable, but it's also a cleaner technique, which, among other things, protects again callback hell. Think of this Message
object as the representation of a single message box.
Step 4
If Message
represents a single message box, what will be the HTML for one? Let's create a div
with a class of message-box
, and make it available to the Message
instance, via an el
property.
var Message = { initialize: function(text) { this.el = $('<div>', { 'class': 'message-box', 'style': 'display: none' }); this.text = text; return this; } };
Now, the object has an immediate reference to the wrapping div
for the message box. To gain access to it, we could do:
var msg = Object.create(Message).initialize(); // [<div class=​"message-box" style=​"display:​ none">​</div>​] console.log(msg.el);
Remember, we now have an HTML fragment, but it hasn't yet been inserted into the DOM. This means that we don't have to worry about any unnecessary reflows, when appending content to the div
.
Step 5
The next step is to take the provided message string, and insert it into the div
. That's easy!
initialize: function(text) { // ... this.el.html(this.text); } // [<div class=​"message-box" style=​"display:​ none">​Here is an important message​</div>​]
However, it's unlikely that we'd want to insert the text directly into the div
. More realistically, the message box will have a template. While we could let the user of the plugin create a template and reference it, let's keep things simple and confine the template to the Message
object.
var Message = { template: function(text, buttons) { return [ '<p class="message-box-text">' + text + '</p>', '<div class="message-box-buttons">', buttons, '</div>' ].join(''); // ... };
In situations, when you have no choice but to nest HTML into your JavaScript, a popular approach is to store the HTML fragments as items within an array, and then join them into one HTML string.
In this snippet above, the message's text is now being inserted into a paragraph with a class of message-box-text
. We've also set a place for the buttons, which we'll implement shortly.
Now, the initialize
method can be updated to:
initialize: function(text) { // ... this.el.html(this.template(text, buttons)); }
When triggered, we build the structure for the message box:
<div class="message-box" style="display: none;"> <p class="message-box-text">Here is an important message.</p> <div class="message-box-buttons></div> </div>
For more complex projects, consider using the popular Handlebars templating engine.
Step 6
For the message box to be as flexible as possible, the user of the plugin needs to have the ability to optionally specify, among other things, which buttons should be presented to the user - such as "Okay," "Cancel," "Yes," etc. We should be able to add the following code:
$.message('Are you sure?', { buttons: ['Yes', 'Cancel'] });
...and generate a message box with two buttons.
To implement this functionality, first update the $.message
definition.
$.message = function(text, settings) { var msg = Object.create(Message); msg.initialize(text, settings); return msg; };
Now, the settings
object will be passed through to the initialize
method. Let's update it.
initialize: function(text, settings) { this.el = $('<div>', {'class': 'message-box', 'style': 'display: none'}); this.text = text; this.settings = settings this.el.html(this.template(text, buttons)); }
Not bad, but what if the plugin user does not specify any settings? Always consider the various ways that your plugin can be used.
Step 7
We assume that the user of the plugin will describe which buttons to present, but, to be safe, it's important to provide a set of defaults, which is a best practice when creating plugins.
$.message = function(text, settings) { var msg = Object.create(Message); msg.initialize(text, settings); return msg; }; $.message.defaults = { icon: 'info', buttons: ['Okay'], callback: null };
With this approach, should the plugin user need to modify the defaults, he only needs to update $.message.defaults
, as needed. Remember: never hide the defaults from the user. Make them available to "the outside."
Here, we've set a few defaults: the icon, buttons, and a callback function, which should be triggered, once the user has clicked one of the buttons in the message box.
jQuery offers a helpful way to override the default options for a plugin (or any object, really), via its extend
method.
initialize: function(text, buttons) { // ... this.settings = $.extend({}, $.message.defaults, settings); }
With this modification, this.settings
will now be equal to a new object. If the plugin user specifies any settings, they will override the plugin's defaults
object.
Step 8
If we intend to add a custom icon to the message box, dependent upon the action, it'll be necessary to add a CSS class to the element, and allow the user to apply a background image, accordingly.
Within the initialize
method, add:
this.el.addClass('message-box-' + this.settings.icon);
If no icon is specified in the settings
object, the default, info
, is used: .message-box-info
. Now, we can offer a variety of CSS classes (beyond the scope of this tutorial), containing various icons for the message box. Here's a couple examples to get you started.
.message-box-info { background: url(path/to/info/icon.png) no-repeat; } .message-box-warning { background: url(path/to/warning/icon.png) no-repeat; }
Ideally, as part of your MessageBox plugin, you'd want to include an external stylesheet that contains basic styling for the message box, these classes, and a handful of icons.
Step 9
The plugin now accepts an array of buttons to be applied to the template, but we haven't yet written the functionality to make that information usable. The first step is to take an array of button values, and translate that to the necessary HTML inputs. Create a new method on the Message
object to handle this task.
createButtons: function(buttons) {}
jQuery.map
is a helpful method that applies a function to each item within an array, and returns a new array with the modifications applied. This is perfect for our needs. For each item in the buttons array, such as ['Yes', 'No']
, we want to replace the text with an HTML input, with the value set, accordingly.
createButtons: function(buttons) { return $.map(buttons, function(button) { return '<input type="submit" value="' + button + '">'; }).join(''); }
Next, update the initialize
method to call this new method.
initialize: function(text, settings) { this.el = $('<div>', {'class': 'message-box', 'style': 'display: none'}); this.text = text; this.settings = $.extend({}, $.message.defaults, settings); var buttons = this.createButtons(this.settings.buttons); this.el.html(this.template(text, buttons)); return this; }
Step 10
What good is a button, if nothing happens when the user clicks it? A good place to store all event listeners for a view is within a special events
method on the associated object, like this:
initialize: function() { // ... this.el.html(this.template(text, buttons)); this.events(); }, events: function() { var self = this; this.el.find('input').on('click', function() { self.close(); if ( typeof self.settings.callback === 'function' ) { self.settings.callback.call(self, $(this).val()); } }); }
This code is slightly more complex, due to the fact the user of the plugin needs to have the ability to trigger their own callback function, when a button is clicked on the message box. The code simply determines whether a callback
function was registered, and, if so, triggers it, and sends through the selected button's value. Imagine that a message box offers two buttons: "Accept" and "Cancel." The user of the plugin needs to have a way to capture the clicked button's value, and respond accordingly.
Notice where we call self.close()
? That method, which has yet to be created, is responsible for one thing: closing and removing the message box from the DOM.
close: function() { this.el.animate({ top: 0, opacity: 'hide' }, 150, function() { $(this).remove(); }); }
To add a bit of flair, upon hiding the message box, we, over the course of 150 milliseconds, fade out the box, and transition it upwards.
Step 11
The functionality has been implemented! The final step is to present the message box to the user. We'll add one last show
method on the Message
object, which will insert the message box into the DOM, and position it.
show: function() { this.el.appendTo('body').animate({ top: $(window).height() / 2 - this.el.outerHeight() / 2, opacity: 'show' }, 300); }
It only takes a simple calculation to position the box vertically in the center of the browser window. With that in place, the plugin is complete!
$.message = function(text, settings) { var msg = Object.create(Message).initialize(text, settings); msg.show(); return msg; };
Step 12
To use your new plugin, simply call $.message()
, and pass through a message and any applicable settings, like so:
$.message('The row has been updated.');
Or, to request confirmation for some destructive action:
$.message('Do you really want to delete this record?', { buttons: ['Yes', 'Cancel'], icon: 'alert', callback: function(buttonText) { if ( buttonText === 'Yes' ) { // proceed and delete record } } });
Closing Thoughts
To learn more about jQuery development, you're encouraged to refer to my course on this site, "30 Days to Learn jQuery."
Over the course of building this sample MessageBox
plugin, a variety of best practices have emerged, such as avoiding callback hell, writing testable code, making the default options available to the plugin user, and ensuring that each method is responsible for exactly one task.
While one could certainly achieve the same effect by embedding countless callback functions within $.message
, doing so is rarely a good idea, and is even considered an anti-pattern.
Remember the three keys to maintainable code and flexible jQuery plugins and scripts:
- Could I test this? If not, refactor and split the code into chunks.
- Have I offered the ability to override my default settings?
- Am I following bad practices or making assumptions?
To learn more about jQuery development, you're encouraged to refer to my screencast course on this site, "30 Days to Learn jQuery."
Comments