jQuery plugins aren’t that difficult to write, but some folks seem to have a hard time understanding how to get started, so I thought I would write a quick blog post about that. I’ll show how to make a pretty simple keyword highlighter which also wraps your pre/code elements in a div with a header span element.
I’ve written a few jQuery plugins before. One of my favorites is jquery.empuzzle. This is a favorite not only because it’s incredibly fun, but because it covers a few areas of jquery plugin development:
- default options
- merge options
- custom selectors
- plugin structure
It also covers a particular gotcha when your plugin interacts with images (i.e. images may load after document ready is fired). It’s definitely worth checking it out, but I’d like to show a much simpler plugin here.
The structure
jQuery plugins are commonly written as an *immediate function*. This is creates a closure which executes immediate on the jQuery object itself:
;(function($) {
// your plugin goes here.
})(jQuery);
I often wrap a jQuery plugin in semicolons as you see above to prevent any automatic semicolon insertion errors. This is something I would consider a ‘best practice’ and I suggest you do the same.
Within this structure, you’ll want to extend jQuery to include your plugin’s code.
$.fn is a pointer to the jQuery prototype object. When you execute a jQuery call like this:
var examples = $('.test-elements');
.. you will receive a jQuery object. This object represents an array of results matching the selector passed to the jQuery function. You can use console.log() from within your plugin function to see how you might interact with this object.
;(function($) {
$.fn.example = function(arg) {
console.log(this);
};
})(jQuery);
You now have the start to a plugin called ‘example’. You can execute this ‘plugin’ in the normal way.
var examples = $('.test-elements');
examples.example();
// same as: $('.test-elements').example();
Now, you’ll want to iterate over each object found by the query selector and perform whatever functionality your plugin will provide. Let’s define some functionality so we can have a (somewhat) useful plugin. We’ll keep with the ‘example’ theme and write a plugin which stylizes a tag and adds a small box in the top-left corner which says ‘Example’. (I got this idea from Twitter’s Bootstrap framework, see here). We’ll also ‘highlight’ a few keywords within our example.
Here’s what we want our final product to look like (after some hideous styling, of course):
For the goal screenshot, I’ve used Google’s prettify plugin, which is licensed under Apache 2.0. I don’t use this in the final product.
There will be a few things we’ll need to do in this plugin:
- Merge settings specified by the user with our default settings
- Wrap the code block in a styled ‘example’ box
- “parse” the contents for a select set of keywords
- Wrap any found keywords with a style span
For the parsing bit, I’ll just use a global RegEx replace on the contents of the code element.
Providing defaults
Providing defaults in a jQuery plugin and allowing a user to pass options to merge isn’t difficult. Your plugin function will accept a parameter I like to call ‘opts’ and return a `this.each` function. Within the `.each` function, you’ll extend the defaults object with those options passed into your plugin function and store the merged options into a new object called ‘options’. A very simple example which writes out to console.log might look like this:
;(function($) {
var defaults = {
arr: [1,2,3,4,5,6],
sample: "sample",
style: "style"
};
var example = function(opts) {
return this.each(function() {
// merge opts with defaults into new object
// so changes don't change defaults
var options = $.extend({}, defaults, opts);
console.log({
elem: this.innerText,
options: options
});
});
};
$.fn.example = example;
})(jQuery);
Here are some options we might want to allow for the example plugin:
- A class name for the box and a class name for the label
- An object mapping keywords to class names.
- A parse error callback
- A data attribute to specify a different ‘Example’ box text (so each element can define its own label)
Our defaults object will look like this (more or less):
var defaults = {
boxCss: "example-box",
labelCss: "example-label",
keywords: { "function":"blue", "this":"blue", "jQuery":"red" },
onError: function() { },
exampleAttr: ""
};
Wrapping our targets
To make our lives simple, we’ll first wrap the target element with a div for our example box. Then, we’ll add a span just before the code block. We can improve performance by creating the wrapper div’s HTML outside of the ‘.each’ function. Because we’re allowing the option of pulling the labeling span’s text from the code element itself, we’ll have to build that HTML within the ‘.each’ function.
Here’s what we have so far.
;(function($) {
var defaults = {
boxCss: "example-box",
labelCss: "example-label",
keywords: { "function":"blue", "this":"blue", "jQuery":"red" },
onError: function() { },
exampleAttr: ""
};
var example = function(opts) {
var options = $.extend({}, defaults, opts);
var wrapperHtml = [
'<div class="',
options.boxCss,
'"></div>'
].join('');
var labelArr = [
'<span class="',
options.labelCss,
'">',
"Example",
'</span>'
];
return this.each(function() {
var labelText = "Example";
if(options.exampleAttr) {
labelText = $(this).attr(options.exampleAttr);
}
var labelHtml = [
'<span class="',
options.labelCss,
'">',
labelText,
'</span>'
].join('');
$(this).wrap(wrapperHtml);
$(this).before(labelHtml);
});
};
$.fn.example = example;
})(jQuery);
onError function
The onError function we allow in the options object does nothing more than provide a message if we don’t have a code element to update with keyword highlights. We will supply a message to the callback function and return from the current iteration of ‘.each’. This goes at the beginning of the ‘this.each’ function.
// Find node for replacing text.
var replacementNode = $(this).children('code').andSelf().filter('code');
if(replacementNode.length == 0) {
if(typeof options.onError === "function"){
options.onError("No code nodes found");
}
return;
}
With the combination of children/andSelf/filter in the above code, we allow the plugin to operate on both ‘pre’ and ‘code’ elements.
When allowing users to pass functions as callbacks or to provide added functionality to a plugin, *always* check that it is a function.
“Parsing” contents
In the interest of saving some time, I’m going to use a regular expression to replace text within the code block. This isn’t necessarily the most efficient way to achieve a budget syntax highlighter, but it will work.
To do this, we’ll need to grab the text from our code node in which we’ll replace keywords. Then, for every keyword in the hash of keywords-to-classes, we’ll create a span to represent the highlighted keyword. Then, we’ll do a search and replace using a global regular expression object, substituting each found keyword with the keyword wrapped in a span element. This goes at the end of the ‘this.each’ function.
var originalText = replacementNode.text();
Object.keys(options.keywords).forEach(function(key,idx){
var replacement = [
'<span class="',
options.keywords[key],
'">',
key,
'</span>'
].join('');
var re = new RegExp(key,"g");
originalText = originalText.replace(re, replacement);
});
The only thing left to do is to add styles to your document and you’re all set with a customized syntax highlighter!
There are a few issues with this simple implementation. First, keywords don’t get merged (I’ll leave that as an exercise for you). Second, this only highlights keywords. It doesn’t provide regex matching for full-text highlights. In other words, this won’t highlight comments.
If you’re unfamiliar with jQuery plugins, or you’re planning to begin writing a keyword highlighter plugin, this should at least get you started!
Try it out!
Check out the jsfiddle.
Get the code
The code for this blog post is available on Github.