How to build a web widget (using jQuery)
Published on 15 June 2010, updated on 10 February 2015, Comments
Introduction
I created some web widgets for the London’s Design Museum and learned a few useful things in the process. Although all the necessary information can be found on the web, I couldn’t find a really comprehensive guide about doing this with jQuery so I decided to write this one. I’ll cover only techniques which are specific to web widgets so you should already be familiar with JavaScript, jQuery and web development if you want to follow easily.
The interesting points will be:
- ensure the widget’s code doesn’t accidentally mess up with the rest of the page,
- dynamically load external CSS and JavaScript files,
- bypass browsers’ single-origin policy using JSONP.
Don’t worry if all this doesn’t make sense to you yet, we’ll see in details what these techniques are used for in the context of building web widgets.
What is a web widget?
A web widget is a component, a “chunk of web page” that you provide for other people to display data coming from your site on their own pages. A widget typically contains a slightly complex mixture of HTML, CSS and JavaScript. You will want to hide that complexity and make it as easy as possible for people to include your widget on their pages.
Widget usage
The widget developed in this tutorial can be embedded in a web page with just
two HTML tags: one script
tag to load the widget and one container tag
(usually a div
) to indicate where we want to put the widget on the page:
<script src="http://example.com/widget/script.js" type="text/javascript"></script>
<div id="example-widget-container"></div>
That’s really all that a web site owner would need to include our widget on her
pages. The code referenced with the script
tag will take care of downloading
the different assets composing the widget and update the content of the
container.
Can’t it be even simpler?
It is technically possible to create a widget that doesn’t require a
destination container by using document.write()
within the widget’s
code. Although some widget providers do use that approach, and it can reduce
the code necessary on the host page to just one script
tag, we believe it’s not
worth it because:
-
document.write()
cannot be called once a page has loaded. This means our widget’s code would have to run immediately when the browser finds the script tag. Since we want this code to fetch data from our server, that could cause page loading to be interrupted for a while and it would make the page feel slow, - by using a separate tag which will contain the widget on the page, we are free to place our script tag anywhere. We can for instance put it at the bottom of the page, which is a recommended practice.
Code isolation
Because you can’t predict what JavaScript code will be running on the page which uses our widget, we need a way to ensure that it doesn’t clash with any other JavaScript code included on the host page. To do that, we just enclose all our code within an anonymous function and we call that function. The variables we create in our functions won’t interfere with the rest of the page.
Here is a simple example using this technique:
var foo = "Hello World!";
document.write("<p>Before our anonymous function foo means '" + foo + '".</p>');
(function() {
// The following code will be enclosed within an anonymous function
var foo = "Goodbye World!";
document.write("<p>Inside our anonymous function foo means '" + foo + '".</p>');
})(); // We call our anonymous function immediately
document.write("<p>After our anonymous function foo means '" + foo + '".</p>');
You can view the result in your browser. As
you can see the foo
variable defined in an anonymous function doesn’t clash with the global foo
variable.
Always declare variables with var
In order to avoid tampering with JavaScript variables defined outside of our
widget, we must declare all our variables with the var
keyword, otherwise
we might be updating existing variables instead of creating our own. It’s anyway
a good practice to use var
every time you create a variable.
Loading JavaScript libraries
If your widget will use a fair amount of JavaScript, you’ll likely want to use a JavaScript framework such as jQuery. Since we can’t rely on having jQuery loaded on the host page, we’re going to load jQuery dynamically using JavaScript:
(function() {
// Localize jQuery variable
var jQuery;
/******** Load jQuery if not present *********/
if (window.jQuery === undefined || window.jQuery.fn.jquery !== '1.4.2') {
var script_tag = document.createElement('script');
script_tag.setAttribute("type","text/javascript");
script_tag.setAttribute("src",
"http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js");
if (script_tag.readyState) {
script_tag.onreadystatechange = function () { // For old versions of IE
if (this.readyState == 'complete' || this.readyState == 'loaded') {
scriptLoadHandler();
}
};
} else { // Other browsers
script_tag.onload = scriptLoadHandler;
}
// Try to find the head, otherwise default to the documentElement
(document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
} else {
// The jQuery version on the window is the one we want to use
jQuery = window.jQuery;
main();
}
/******** Called once jQuery has loaded ******/
function scriptLoadHandler() {
// Restore $ and window.jQuery to their previous values and store the
// new jQuery in our local jQuery variable
jQuery = window.jQuery.noConflict(true);
// Call our main function
main();
}
/******** Our main function ********/
function main() {
jQuery(document).ready(function($) {
// We can use jQuery 1.4.2 here
});
}
})(); // We call our anonymous function immediately
First we create a local variable which will hold our version of jQuery. Then we
check for the presence of the version of jQuery we need, so that we don’t
download it again if by chance it’s already there on the host page. Here we’re
being strict about the version number, but if you’ve tested your code with
several versions, you can allow these versions as well, or even use a regular
expression to match a major version number. To load the library, we then simply
create a script
element which points to the URL of the library file. In case
we’re dealing with a poorly written page which doesn’t have a head
, we
simply append our script
tag to the document element.
Of course, we want to use the library in our code, but before we do so, we must
be sure that the library has loaded completely. We do that by defining a
scriptLoadHandler
function which will be called once the library has loaded.
For most browsers (Firefox, Safari, Opera, Chrome, etc.) we just need to assign
that function to the onload
attribute of the script
element. Internet
Explorer before version 9, as often, has its own way. Here it requires us to
register an onreadystatechange
handler which will call our main function when
the ready state is either complete
or loaded
. As people helpfully pointed out
in the comments, IE9 and Opera support onload
, but also still support
onreadystatechange
, so we make sure that we use only one of them by testing
the presence of a readyState
attribute on the script tag, in
order to avoid calling our load handler twice.
Why test for two states in Internet Explorer’s old onreadystatechange
handler?
Once the script is ready to use, Internet Explorer will set readyState
to
either complete
or loaded
, depending if it’s loaded from the cache or
downloaded from the network. For instance, if you test only for loaded
, the
main function won’t be called if you navigate away from the page by clicking a
link and then come back to it using the back button of your browser.
In our scriptLoadHandler
function we make sure that jQuery can be used with other
libraries and also
with other versions of itself. The following line deserves particular attention:
jQuery = window.jQuery.noConflict(true);
Before this line gets executed, window.jQuery
points to our own version of jQuery which
would have overwritten any older version of the library. Calling
window.jQuery.noConflict(true)
restores window.jQuery
to its old value and
returns a reference to our freshly loaded version, which we store in our local
variable. It also restores the $
variable to its old value, so that we can
still use another library such as Prototype.
Finally, scriptLoadHandler
calls our main
function in which we can use
our version of jQuery and build our widget.
Here are two examples of this technique. In both examples Prototype has been loaded. The only difference between the two is that here another version of jQuery has been loaded whereas here the same version of jQuery has been loaded.
Now that we’ve dealt with the underlying plumbing, we can finally get to the interesting part.
Loading data from our site
Since our widget is going to display data on the host page as HTML, there is probably no need to use an intermediate format such as XML or JSON to get data from our server. We can directly get HTML and update the content of the widget container using that HTML.
Let’s say our widget is going to display a list of headlines. The server can output a chunk of HTML such as:
<ul>
<li><a href="http://example.com/a-first news">A first news</a></li>
<li><a href="http://example.com/another-news">Another news</a></li>
<li><a href="http://example.com/an-interesting-news">An interesting news</a></li>
</ul>
If the host page was located in the same domain we could just use regular AJAX (or to be pedantic in this case AHAH) to get this HTML and update the DOM. But in the context of a web widget, the host page and the server providing the widget’s data will usually be in different domains and browsers will simply not allow an AJAX request to be made to a different domain than the one which has served the page. This security restriction is know as the Same Origin Policy.
Luckily, there is a way around this: browsers allow to use script tags to
include scripts from other domains. We can use this open door to get data from
our server using a simple technique known as
JSONP. The idea of JSONP is to
create a script
tag which will fetch JSON data wrapped into a function.
If we create a script tag such as:
<script type="text/javascript" src="http://example.com/widget/data.js"></script>
The content of http://example.com/widget/data.js
will look like this:
our_callback( {"foo": 42, "bar": 23 } )
We then define the function which will get the JSON data as a parameter:
function our_callback(json_data) {
// do stuff with json_data
}
Now the good news is that jQuery supports JSONP natively and will take care for us of creating a script tag, creating a callback function and pass the name of that callback to the server as a parameter. In this case the only data we’re interested in is the piece of HTML we want to insert in the widget container. On the server side, we will use that parameter to build a piece of JSONP such as:
callback_name_generated_by_jquery( {"html": "<ul><li>(...)</li></ul>" } )
Here is an example of generating JSONP on the server using Ruby on Rails:
# Store HTML in a variable rather than returning in to the browser
html = render_to_string
# Build a JSON object containing our HTML
json = {"html" => html}.to_json
# Get the name of the JSONP callback created by jQuery
callback = params[:callback]
# Wrap the JSON object with a call to the JSONP callback
jsonp = callback + "(" + json + ")"
# Send result to the browser
render :text => jsonp, :content_type => "text/javascript"
In the client code, it will look like a normal AJAX call returning JSON.
var widget_url = "http://example.com/wiget_data.js?callback=?"
$.getJSON(widget_url, function(data) {
$('#example-widget-container').html(data.html);
});
Don’t forget to include a string such as callback=?
in your URL, otherwise the server won’t receive the name of the callback as a parameter.
We are now able to retrieve HTML from our server and insert it in the widget container on the host page. We’re pretty much done, we might just want to add a bit of styling.
Loading CSS
To load our stylesheets, we just create a link tag using JavaScript:
var css_link = $("<link>", {
rel: "stylesheet",
type: "text/css",
href: "style.css"
});
css_link.appendTo('head');
Putting things together
Now that we’ve seen the different bits we need to build a web widget, we can put everything together and build a simple example. Here is the JavaScript code:
(function() {
// Localize jQuery variable
var jQuery;
/******** Load jQuery if not present *********/
if (window.jQuery === undefined || window.jQuery.fn.jquery !== '1.4.2') {
var script_tag = document.createElement('script');
script_tag.setAttribute("type","text/javascript");
script_tag.setAttribute("src",
"http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js");
if (script_tag.readyState) {
script_tag.onreadystatechange = function () { // For old versions of IE
if (this.readyState == 'complete' || this.readyState == 'loaded') {
scriptLoadHandler();
}
};
} else {
script_tag.onload = scriptLoadHandler;
}
// Try to find the head, otherwise default to the documentElement
(document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
} else {
// The jQuery version on the window is the one we want to use
jQuery = window.jQuery;
main();
}
/******** Called once jQuery has loaded ******/
function scriptLoadHandler() {
// Restore $ and window.jQuery to their previous values and store the
// new jQuery in our local jQuery variable
jQuery = window.jQuery.noConflict(true);
// Call our main function
main();
}
/******** Our main function ********/
function main() {
jQuery(document).ready(function($) {
/******* Load CSS *******/
var css_link = $("<link>", {
rel: "stylesheet",
type: "text/css",
href: "style.css"
});
css_link.appendTo('head');
/******* Load HTML *******/
var jsonp_url = "http://al.smeuh.org/cgi-bin/webwidget_tutorial.py?callback=?";
$.getJSON(jsonp_url, function(data) {
$('#example-widget-container').html("This data comes from another server: " + data.html);
});
});
}
})(); // We call our anonymous function immediately
And here is an example of implementation on the server, using a simple CGI script written in Python:
#!/usr/bin/python
import cgi
params = cgi.FieldStorage()
print "Content-Type: text/javascript\n"
if not 'callback' in params:
print "ERROR: you must pass a callback parameter"
else:
jsonp = "%s ( {'html': '<strong>Hello World!</strong>' } )"
print jsonp % params['callback'].value
Of course, real world widgets will be much more complex, both on the client and on the server, but hopefully you now have a better understanding of the basic steps involved.
Dealing with errors on host pages
Although the code above will work in a lot of cases, Shyam Subramanyan from Listly has
reported an issue with using jQuery(document).ready
in the main
function.
The problem can happen when the host page already has loaded jQuery and has its own
jQuery(document).ready
hooks. If there are any errors in any of the host page
jQuery hooks, the one inside the widget main
function will never get executed.
The solution Shyam suggests is to write custom code to test if the page is ready to host the widget. Instead of waiting for the whole document to be ready, only check for the presence of DOM elements or other JavaScript objects that your widget depends on. The code he’s been successfully using for Listly looks like this:
Listly.listlyReady = function () {
// Check for presence of required DOM elements or other JS your widget depends on
if (Listly.jQuery.listlybox && ListlyAuth) {
window.clearInterval(Listly._readyInterval);
// Make stuff here
}
};
// This our new main function
function main () {
Listly._readyInterval = window.setInterval(Listly.listlyReady, 500);
}
According to Shyam, this type of code has been working fine for Listly and more importantly, the widget typically loads much quicker especially in cases where the host page is heavy or has errors.
Conclusion
A lot of people have asked how to load plugins, so I’ve wrote a separate article about this.
Another question that comes often is how can you make your widget configurable. As suggested in the comments, if you want to minimize markup you could use the query string of your widget URL and add an id to the script tag. By doing this your script would be able to locate its own script tag, parse the query string of its own URL and configure itself. A more straightforward approach could be to simply add some invisible markup to the embed code of your widget. That’s how social sharing buttons typically do it.
I hope this tutorial has been of interest. Thank you everyone for reading and special thanks to Corey Hart for his valuable help with dynamic loading of jQuery. Make sure you browse the comments as some of them contain useful advice about how to make your widget customizable, how to reduce even further the size of the embed code, etc.
If you want to dig deeper into the topic, I highly recommend the book Third-Party JavaScript. I found it to be a very pleasant and informative read.