jquery gadget plugin
The jquery.gadget plugin provides a way of combining javascript with a html resource and css, providing a simple, scoped lifecycle and a standardised approach to writing modular, self contained ajax gadgets. It’s an intentionally simple approach to a problem that sits somewhere between basic jquery ajax such as $.get(”/url.html”) and the more complicated mvc style approach of frameworks like sproutcore, javascriptMVC etc.
Typically a gadget will be comprised of three parts:
- A javascript file – typically gadgets exist in their own file and are named in relation to their file location and url of the html resource. This helps to keep things clear within the project. As the number of gadgets in your project increases you’ll want to start combining gadget js files into a single minified resource. Packtag is a good solution to this problem in the java world. There are other tools for this in other languages.
- A html resource – this is the server generated html referenced and loaded by the gadget.
- A css file - css used to style the gadget. The same naming and organization rules apply as for the gadget js.
While the css is an important part of the picture, there is no specific functionality provided for it within the gadget framework. There are however a series of ‘best practices’ that describe simple ways of scoping your css to your gadgets so that they are isolated from the rest of your page and will not cause problems when combined with other elements on the page. These are provided later in the document.
Registering a gadget
Before a gadget can be used, it must be registered with the page. This is a little like writing a class and it’s the way you define the behaviour of the gadget common to all instances.
A gadget should be registered in such a way that it’s both correctly scoped and evaluated immediately on load as opposed to on dom-ready. This ensures that the gadget is added to the gadget registry so it can be instantiated within the page. The recommended way of registering a gadget is as follows:
(function() {
$.gadget.register("my_gadget", {
baseUrl : my_gadget.html",
initialize : function() {
var gadget = this;
// do things with the gadget here
}
});
})();
The three key pieces here are:
- The gadget name – this must be unique within the page, most commonly this really means within the application. If a page contains more than one gadget registered under the same name, an error will be raised in the console.
- The baseUrl – this is the base url of the html for the gadget. Don’t put query string params here. They are passed separately and generally when creating instances of the gadget rather than during registration as these vary between instances.
- initialize – this is called once the gadget’s html has been inserted into the dom. This is where you define any common behaviour such as binding events to elements within the gadget.
Typically when a gadget is initialised, you will want to act on elements within the gadget html. In fact, as a general rule, you should only ever act on dom elements that are known to the gadget in order to keep coupling as low as possible. Cross gadget communication will be covered later on when we discuss events.
Once initialised, every gadget will have a reference to the dom element that wraps the gadget html. This property is exposed directly from the gadget instance and is called ‘element’ – it is also important to note that this is a jquery object wrapper of the dom element. So for example, if you wanted to find all button elements within the gadget onload and bind to their onclick event you could do something like this:
var gadget = this; gadget.find("button").click(function() { console.log("Click!"); });
Note the shortcut to the jquery find method. You could also write this as jquery.element.find() but it’s used so often this will save quite a few bytes!
Ok, so we’ve registered a gadget, how do we actually use it ?
Using a gadget
The gadget api is a jquery plugin so it behaves like any other jquery plugin in that it acts on an element or set of elements selected using a jquery dom query. The one difference difference here is that where a regular jquery plugin will return the jquery object it was applied to, the gadget plugin returns the gadget element instead. The reason for this is due to the different ways you can create a gadget in relation to the original element. Also in the context it’s used, it’s unlikely you’d want to be doing any serious chaning.
Say you wanted to insert an instance of the “my_gadget” gadget into the div with id “gadget_container”. In this example you would do this:
$("#gadget_container").gadget("my_gadget");
The result of this would be call the url specified in the registration of the “my_gadget” gadget, insert it into the div with id “gadget_container”, then call the initialize function on the gadget.
There are several options that can be passed to the gadget when creating an instance which are listed below. It is important to note that all of these can be supplied upon registration of the gadget as well. In many cases that is the preferred place as the only configuration that should be supplied upon gadget creation is the config that varies on a per instance basis:
- cache [true / false] – defaults to false – this provides a local cache of the html after the first time it’s called. This is handy when you have some static html that should only ever be loaded once within a page or if you’re loading a html template that you’re going to merge with data upon load.
- method – ["html", "after", "before", "append", "prepend"] – defaults to “html” – determines where the element is inserted in relation to the element that gadget was called upon. Based on the standard jquery dom manipulation methods.
- baseUrl – if you want to override on a per instance basis.
- urlParams – an object literal representing the parameters to be passed with the baseUrl – Eg.
{id: myId} - dataany data you want to pass to the gadget for use in setting up the instance of the gadget etc. Can be retrieved directly from the gadget in the initialise method via this.data.
- postInitialize – provides a place to call specific logic after the main initialisation has occured.
- dialog – a config object or function returning a config object for loading the gadget inside a jquery-ui dialog. This requires jquery-ui to be included on the page or it won’t work.
- cssClass – an optional class name(s) that will be added to the gadget wrapper div.
The structure of a gadget
The basic structure of gadget html is extremely simple. When a gadget is created a wrapper div is created that the gadget html is inserted into. An empty gadget called “contact_list” would look like the following:
<div class="gadget gadget_contact_list"></div>
It might be worth noting here that this root gadget element also has a reference to the gadget object stored using the jquery data construct. You could for instance get a handle to the gadget object in the following way via the root gadget element:
$(".gadget_contact_list").data("gadget");
Gadget CSS
As mentioned earlier there is no specific provision for css in the gadget framework. Following this simple set of rules should help you to write gadgets with properly scoped css.
- Use a single css file for each gadget. If you are doing this, we highly recommend combining all your css into a single file as part of your build process. Better still, use a framework like packtag (for java) or one of the solutions for other back-ends to do this at runtime. This will make for a much more pleasant development experience, I promise!!
- A gadget automatically gets the following classes applied to it’s wrapper div – “gadget” and “gadget_name_gadget”. Use these classes to correctly scope each gadget css to the gadget. For example:
.user_list_gadget h2 { font-size: 1.2em;}; - This would apply to the following gadget html wrapper element:
<div class="gadget user_list_gadget">//your gadget in here...</div>
It is also possible to pass other css classes via the config option ‘cssClass’ when both registering or creating a gadget. This could be useful if you want to define a class that is shared across multiple gadgets.
Events and inter-gadget-communication
In order for gadgets to stay decoupled from one another each gadget should be able to exist without the requirement that another gadget exist. Sometimes gadgets do however have behaviour that depends on other gadgets so the key in this situation is to limit the communication channel between each gadget to an events based system. One gadget can publish and event that other gadgets can listen for – the end result being that the only coupling between the gadgets is an event name.
This is probably best illustrated with a scenario:
Say you have a typical master-detail view which is comprised of two gadgets, one is used to represent the a list of contacts and the other a specific contact. When the user clicks on a contact from the list, the contact should be opened in the detail view.
The wrong way:
On click of the contact in the list, the html for the contact is fetched, a dom query is made from the list gadget that finds the container for the detail view container and the html is inserted directly into this container.
The problem with this approach is that the list view requires that the detail view exist on the page to work. They are coupled together. It would be difficult or impossible to test these two gadgets in isolation.
A better way:
On click of the contact in the list, the “contact_loaded” event is fired with the id of the contact to open. The detail view gadget is listening for this event so when it receives notification, it takes the id for the contact, loads the contact, and then populates it’s content with the contact details. Of course there are a few ways you could achieve this depending on the context but this example serves mainly to illustrate the use of events for the communication between the gadgets than the implementation of this specific scenario.
Binding and triggering events
The gadget framework makes use of jquery custom events. This essentially boils down to binding functions to named events at a global level. The trick with gadgets or in fact any heavy ajax based page is that you don’t want event bindings to hang around longer than the gadget that they are owned by, or just as importantly, to element that the listeners refer to.
Custom jquery events can be bound directly to jquery objects at any level of the dom. Unlike regular dom events however, custom events do not bubble, so you need to trigger an event on the same element that the listener is bound to. Binding events in this way somewhat defeats the purpose though because it means event publishers need to know what objects are listening to them and trigger each one explicitly. One way around this, and the one that gadgets use is to bind and trigger all events against $(document). This effectively provides a global event hub that all custom events pass through.
For this to work correctly, you need to be sure to namespace your events correctly so you don’t have collisions. Using some basic conventions this can be pretty straight forward. I’ve found that prefixing each event with the gadget name is a good start but any convention is fine.
To extend our previous example, if upon initialising itself, our contact gadget binds itself to any events called “contact_loaded”, there is now a function bound to the gadget registry. If we were to then do something with the page that caused the dom element representing the gadget to be removed from the dom the binding for the “contact_loaded” event will still be in the event registry. Now we come back to the contacts view and another instance of the same event function gets added to the registry. Now when we go to open a contact, the event is picked up by two identical functions which are both executed. Worse still, the first instance of the function is refering to a dom element that no longer exists. This is obviously not something that we want.
The way to avoid this in the gadget world is to use the gadget itself to bind and (less importantly) trigger events. This way the gadget knows of all it’s bindings and if used correctly can clean up after itself. For this to work effectively two things must be done:
1. All bindings to external events from a gadget should be done in the following manner:
gadget.bind("event_name", function(event, data){});
2. Whenever updating content within a page, use a gadget to do it unless you are absolutely sure that their are no child nodes within that content that contain other gadgets. The reason for this is that when a gadget inserts it’s html content into the dom, it checks for any existing gadgets that it might be overwriting. If there are any, it unbinds all event bindings and detroys the gadget. This ensures that there’s no rogue events hanging around outside of the life of the gadget.
Global Gadget Events
There are a couple of global events at this stage. Global events differ from instance events in that they are triggered for any gadget instance.
- preLoad [gadget_pre_load]
- postLoad [gadget_post_load]
These events are published as global jquery events under the keys shown above in brackets. These events could be used as a gadget focused way of displaying a loading indicator for instance. Both events pass a reference to the gadget. For example:
$(document).bind("gadget_pre_load", function(event, gadget) {
showLoadingIndicator(gadget.element);
});
Where to from here ?
There are lots of places you can take something like this, however, much further and you’re approaching functionality that is much better supported by ‘real’ frameworks. Some simple things that could be added and probably will before too long are:
- Support for templating so that instead of returning populated html, a html template is returned and a seperate json request returns the data that can be merged with the html template. This way the html can be cached and a smaller request for data can result in a snappier application.
Feel free to do whatever you like with this code. Use it as-is or pull it apart. Internally there are things i’ll probably change before too long as it was originally written a while back and i’ve learned a thing or two about javascript that i’d do differently. I will probably write a version of this that is based on jquery-ui widgets at some stage as they enforce a nicer set of conventions that this could benefit from.
Get the code
You can download the latest code from here.
Credits & feedback
Lastly i’d like to thank Jonathan Ricketson who wrote the orginal version of this some time back.
If you choose to use this, i’d love to hear about it, or if you have any comments feel free to email me at robmonie [a.t] redredred dot com dot au