Back in January this year jQuery announced a new
, so now seemed like a great time to write a tutorial combining building a jQuery plugin with my passion – realtime web technologies.
Realtime web technologies make it really easy to add live content to previously static web pages. Live content can bring a page alive, retain users and remove the need for them to refresh the page periodically. Realtime updates are generally achieved by connecting to a source of data, subscribing to the data you want to add to the page and then updating the page as the data arrives. But why can’t this be achieved through simply marking up a page to identify what data should be shown and where? Well, maybe it can!
jQuery’s tagline is write less, do more. The tagline for the jQuery Realtime plugin that we’re going to build in this tutorial will be write less, do realtime.
In this tutorial we’ll create a jQuery plugin that makes it really easy to add realtime content to a page by simply adding some markup. First, we’ll cover how to use a service called
to subscribe to realtime data. Then we’ll define a way of marking up an HTML5 document with ‘data-*’ attributes in a way which can then be queried by our realtime jQuery plugin and converted to realtime data subscriptions. Finally, we’ll create the jQuery plugin which will use the attributes to subscribe to data and instantly display updates within the page.
If you just want to dive straight in you can
in action or you can and start hacking.Pusher basics
Pusher is a hosted service that makes it easy to add realtime content and interactive experiences to web and mobile apps. Here we’re going to simply connect, subscribe to some data and then update a page when the data comes in.
To demonstrate this create a file called ‘example.html’ and include the Pusher JavaScript library from the Pusher CDN. We know we’re going to use jQuery 2.0.0 so we should also include that now:<!DOCTYPE HTML>
<html> <head> <meta charset="utf-8"> <title>Creating a realtime jQuery plugin | Webdesigner Depot</title> </head> <body> <script src="http://js.pusher.com/2.0/pusher.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script> </body> </html>
Connect
Once the Pusher JavaScript library has been included we can connect to Pusher by creating a new ‘Pusher’ instance and passing in an application key. Create an additional ‘<script>’ tag beneath the jQuery include as follows:
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>Creating a realtime jQuery plugin | Webdesigner Depot</title> </head> <body> <script src="http://js.pusher.com/2.0/pusher.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script> <script> ( function( $ ) { var pusher = new Pusher( '7b9f9ade2135118a915b' ); } )( jQuery ); </script> </body> </html>
Note: For the tutorial we’ll use an application key that I’ve provided but for your own applications you’ll need to sign up to Pusher to get your own.
You can check that you’re connected in three different ways. You can do it manually by checking the
, if you load the page with the Pusher Debug Console open you’ll see the connection logged. The Pusher JavaScript library provides a that you can assign a function to and then you can manually check to make sure a connection has been established by inspecting the browser’s JavaScript console. Or you can check the connection programmatically by of the Pusher instance.
The Pusher Debug console
Whatever you choose to do, you’ll now be connected.
Subscribe
Pusher uses the
so to receive data from Pusher you first need to subscribe to it. Pusher uses the term when it comes to subscriptions, so let’s subscribe to a channel called ‘test-channel’.<script> ( function( $ ) { var pusher = new Pusher( '7b9f9ade2135118a915b' ); var channel = pusher.subscribe( 'test-channel' ); } )( jQuery ); </script>
As with connection state, you can check the status of a subscription in a few ways; using the Pusher Debug Console, by checking the output from ‘Pusher.log’ or by binding to the
Using Pusher.log to log pusher-js library information
Bind to events
Those of you who use jQuery will probably be familiar with the idea of binding to events. jQuery does provide shortcuts for some events (e.g. ‘.onclick( <function> )’) but you can also bind to events using ‘.bind(<type>, <function>)’. Pusher follows this convention and you can bind to events to be informed when something updates; when the connection state changes, when a subscription succeeds or when new application data is received. For this example, and with the realtime plugin, we’re interested primarily in the latter.
Let’s bind to a ‘test-event’ on the channel:
<script> ( function( $ ) { var pusher = new Pusher( '7b9f9ade2135118a915b' ); var channel = pusher.subscribe( 'test-channel' ); channel.bind( 'test-event', handleEvent ); function handleEvent( data ) { console.log( 'in handleEvent:' ); console.log( data ); } } )( jQuery ); </script>
When binding to an event you simply identify the event by name and pass in a reference to a function that will be called when that event occurs (is triggered) on the channel.
If you have a Pusher account you can test that the ‘handleEvent’ function is called by using the
enter ‘test-channel’ as the channel name, ‘test-event’ as the event name and some data (‘{ “some” : “data” }’) into the event data text area and click the submit button. You’ll then see the debug information, along with the data you entered, logged to the JavaScript console.
Triggering an event from the Pusher Event Creator and logging it in the JavaScript console
Since the realtime jQuery plugin that we’re building doesn’t publish (trigger) data (it just consumes it) we won’t cover that here. But if you’re interested in finding out more checkout the
Displaying realtime updates
The next thing to consider is displaying the realtime data updates to the user.
For this we’ll need an idea for a simple application; having worked in finance for a few years I’m generally keen to avoid any type of financial example, but Bitcoin has made it interesting and relevant. So, let’s create a very simple display for showing Bitcoin prices.
Note: We’re going to use some fake data. Let’s make sure this doesn’t result in more Bitcoin panic selling!
First, let’s create some HTML where we’ll display the realtime prices. We can pre-populate the display with prices known at the time the page was loaded:
<h1>Bitcoin Fake Prices</h1> <table id="bitcoin_prices"> <thead> <tr> <th> </th> <th>Last</th> <th>Low</th> <th>High</th> <th>Volume</th> </tr> </thead> <tbody> <tr> <td>BTC/USD</td> <td>61.157 USD</td> <td>51 USD</td> <td>95.713 USD</td> <td>66271 BTC / 4734629 USD</td> </tr> </tbody> </table>
Let’s update the JavaScript to subscribe to a more appropriately named channel called ‘btc-usd’ and bind to a ‘new-price’ event:
<script> ( function( $ ) { var pusher = new Pusher( '7b9f9ade2135118a915b' ); var channel = pusher.subscribe( 'btc-usd' ); channel.bind( 'new-price', handleEvent ); function handleEvent( data ) { } } )( jQuery ); </script>
The ‘data’ sent to the ‘handleEvent’ function should also be in a more appropriate format – here’s the JSON:
{ "last": "last value", "low": "low value", "high": "high value", "volume": "volume value" }
Now that we know this we can change the ‘handleEvent’ function to update the appropriate cell in the table:
function handleEvent( data ) { var cells = $( '#bitcoin_prices tbody tr td' ); cells.eq( 1 ).text( data.last ); cells.eq( 2 ).text( data.low ); cells.eq( 3 ).text( data.high ); cells.eq( 4 ).text( data.volume ); }
If you now trigger a ‘new-price’ event on the ‘btc-usd’ channel, using the JSON we defined, the page will update to show the new values.
There are ways of both making this code nicer and, as the page grows to show more data, optimise things. But, we’re going to make it so that realtime data will be added to the page simply by applying markup.
Before we progress, let’s first add a bit of styling to the example. In the ‘<head>’ add the following CSS:
<style> body { font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; } #bitcoin_prices { border-spacing: 0; } #bitcoin_prices thead { background-color: gray; } #bitcoin_prices tbody tr:nth-child(odd) { background-color: #fff; } #bitcoin_prices tbody tr:nth-child(even) { background-color: #ccc; } #bitcoin_prices tbody tr td:nth-child(1) { background-color: yellow; } #bitcoin_prices td { padding: 4px 10px; } </style>
As you can undoubtedly tell, I’m no designer. So please feel free to improve on this.
The “styled” Bitcoin Fake Prices application
Finally, restructure things so we’re set up for building the plugin.
- Create an ‘examples’ directory and within it a ‘bitcoin’ directory.
- Move the ‘example.html’ file to ‘examples/bitcoin’, rename it ‘index.html’.
- Create a ‘src’ directory at the top-level of the project.
The directory structure should now look as follows:
/
examples/
bitcoin/
index.html
src/
examples/
bitcoin/
index.html
src/
We’re now ready to define our realtime markup and build the realtime jQuery plugin.
Realtime markup
The first thing to highlight is that this isn’t a new idea — I worked for a company called
and in 2001 they had a technology known as RTML that let you markup a page so that realtime updates could be applied. The purpose here is to use jQuery to parse the page and then interpret the markup, resulting in subscriptions, event binding and ultimately live content being added to the page.
For our plugin we’ll use HTML5′s
These attributes don’t directly affect the layout or presentation of the page so they’re a great choice for our realtime markup.
The questions we now need to answer about the markup are:
- Where do we put the Pusher application key?
- How do we identify what channels should be subscribed to?
- How do we identify the events that should be bound to on a channel?
- How do we know what data to display in the page, and where?
The first one is relatively easy. Since we need to include our plugin JavaScript file we can add a ‘data-rt-key’ attribute to the ‘<script>’ element for the plugin include and make the value our application key.
Channel subscriptions can be identified by a ‘data-rt-channel’ attribute. This will mean that the element that the attribute is on and all children are related to this subscription.
Event binding can be achieved using a ‘data-rt-event’ attribute where the element with the attribute, and all children are related to this event.
The values to be extracted from the event data can be indicated by a ‘data-rt-value’ attribute.
Putting this all together, update the Fake Bitcoin Prices markup to look as follows:
<h1>Bitcoin Fake Prices</h1> <table id="bitcoin_prices"> <thead> <tr> <th></th> <th>Last</th> <th>Low</th> <th>High</th> <th>Volume</th> </tr> </thead> <tbody> <tr data-rt-channel="btc-usd" data-rt-event="new-price"> <td>BTC/USD</td> <td data-rt-value="last">61.157 USD</td> <td data-rt-value="low">51 USD</td> <td data-rt-value="high">95.713 USD</td> <td data-rt-value="volume">66271 BTC / 4734629 USD</td> </tr> </tbody> </table> <script src="path/to/jquery.realtime.js" data-rt-key="7b9f9ade2135118a915b"></script>
So, from the script tag you can see we’re going to connect to Pusher using the key identified by ‘data-rt-key’. We’re going to subscribe to the ‘btc-usd’ channel and bind to the ‘new-price’ event. When an event is received we’re going to update the appropriate table cell based on the value indicated by ‘data-rt-value’; if the value of the attribute is ‘last’ then the value of the ‘last’ property is taken from the received ‘data’ object and displayed in the cell.
Hopefully what we are trying to achieve is now pretty clear. Let’s start looking at how to create a jQuery plugin.
jQuery plugin basics
The
are pretty good so I won’t go into the details here. We’ll simply concentrate on building the functionality that we need in our plugin.
Before we write any code we should consider how we want to use the plugin. The normal way a plugin functions is that you use jQuery to query the page, and then you execute the plugin functionality against the matched elements.
$( 'a' ).toggle();
The above code would find all ‘<a>’ elements and then execute the ‘toggle()’ functionality on them — probably hiding all anchors, so not the most useful example you’ll ever see.
So, let’s say we would want to use the plugin as follows:
<script> $( function() { $( '#bitcoin_prices' ).realtime(); } ); </script>
Let’s look at creating the expected functionality.
A realtime plugin
First, create a ‘realtime.jquery.js’ file within the ‘src’ directory. This file will contain the plugin functionality. Then add the following to the file as the starting point of our plugin:
( function( $) { $.fn.realtime = function() { console.log( 'realtime!' ); console.log( $( this ).html() ); }; }( jQuery ) );
We can even test this out now. In ‘examples/bitcoin/index.html’ remove the example plugin ‘<script>’ tag and find the one below the jQuery include and replace it with a ‘<script>’ tag which includes the new ‘realtime.jquery.js’ file (don’t forget the ‘data-rt-key’ attribute) and a script block which executes the ‘realtime()’ plugin on the ‘bitcoin_prices’ table:
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="../../jquery.realtime.js" data-rt-key="7b9f9ade2135118a915b"></script> <script> $( function() { $( '#bitcoin_prices' ).realtime(); } ); </script>
If you refresh the page now you’ll see ‘realtime!’ logged to the JavaScript console along with the HTML from the ‘<table>’ element. This is great as it means the plugin is working; we’re successfully executing our plugin functionality on the table identified by the selector we passed in to jQuery.
jQuery plugins and 3rd party libraries
Our realtime plugin relies on a 3rd party library — the Pusher JavaScript library. For the moment we have it included statically within our HTML, but we don’t want to make that a requirement to use the plugin. So, let’s dynamically load it. jQuery provides a way of easily doing this in the form of the
function.
So, let’s load version 2.0 of the Pusher JavaScript library. We’ll load the HTTPS hosted version so that browsers are happy if our plugin is used on a page served over HTTPS (Chrome already blocks attempts to load HTTP hosted scripts in HTTPS pages and Firefox will do in
). I’m going to wrap loading the library in a function as follows:var libraryLoaded = false; function loadPusher() { $.getScript( "https://d3dy5gmtp8yhk7.cloudfront.net/2.0/pusher.min.js" ) .done( pusherLoaded ) .fail( function( jqxhr, settings, exception ) { console.log( 'oh oh! ' + exception ); } ); } function pusherLoaded( script, textStatus ) { libraryLoaded = true; console.log( 'pusher.min.js loaded: ' + textStatus ); } loadPusher();
If you reload the page the ‘pusher.min.js loaded: success’ message will be logged to the console.
As we’re developing it’s always good to have a way of logging information so at this point let’s create a simple ‘log’ function that we can use which just logs to the console. We’ll use this now and also use it for logging Pusher events. The full source of the plugin is now:
( function( $ ) { function log( msg ) { console.log( msg ); } var libraryLoaded = false; function loadPusher() { $.getScript( "https://d3dy5gmtp8yhk7.cloudfront.net/2.0/pusher.min.js" ) .done( pusherLoaded ) .fail( function( jqxhr, settings, exception ) { log( 'oh oh! ' + exception ); } ); } function pusherLoaded( script, textStatus ) { libraryLoaded = true; Pusher.log = log; log( 'pusher.min.js loaded: ' + textStatus ); } $.fn.realtime = function() { log( 'realtime!' ); log( $( this ).html() ); }; loadPusher(); }( jQuery ) );
You’ll also notice that we’ve assigned the ‘log’ function to the ‘Pusher.log’ property. This means we can see the internal Pusher library logging as well as our own.
When should we connect?
Due to the asynchronous nature of loading the library we can’t guarantee that it will have loaded when our plugin is called into action. Unfortunately this makes things a bit more complex than is ideal but we’ll try to solve it in as simple a way as possible.
We need to check to see if the library has loaded — hence the ‘libraryLoaded’ flag — and act appropriately; if the library has loaded we can connect, if it hasn’t we need to queue the execution until it does. Because of this it makes more sense to only create the Pusher instance when we really need it, which is when we actually want to subscribe to data.
Let’s look at how we can do that:
var pending = []; function pusherLoaded( script, textStatus ) { libraryLoaded = true; while( pending.length !== 0 ) { var els = pending.shift(); subscribe( els ); } } function subscribe( els ) { } $.fn.realtime = function() { var els = this; if( libraryLoaded ) { subscribe( els ); } else { pending.push( els ); } };
When the plugin is called we check the ‘libraryLoaded’ flag to see if the Pusher JavaScript library has been loaded. If it has we’re good to go and we can subscribe. If it’s still pending then we need to queue up the subscriptions. We do this by pushing the jQuery collection (‘els’) onto a ‘pending’ array.
Now, connect
Now that we know that the Pusher JavaScript library has loaded and that the page wants to subscribe to data we can create our ‘Pusher’ instance. Because we only want one ‘Pusher’ instance per page we’re going to follow the
and have a ‘getPusher()’:var pusher; function getPusher() { if( pusher === undefined ) { var pluginScriptTag = $("script[src$='jquery.realtime.js']"); var appKey = pluginScriptTag.attr("data-rt-key"); pusher = new Pusher( appKey ); } return pusher; }
This function grabs the plugin script tag by looking for a tag with a ‘src’ attribute that ends with ‘jquery.realtime.js’, and then gets the value of the ‘data-rt-key’ attribute. It then creates a new ‘Pusher’ instance, passing in the key. As discussed earlier, creating a new ‘Pusher’ instance results in a connection to the source of our data being established.
Subscribe
We can now use the ‘getPusher()’ function anytime we want to access the ‘Pusher’ instance. In our case we want to use it when we parse the elements to determine subscriptions.
Update the placeholder ‘subscribe’ function and add the additional functions shown below:
function subscribe( els ) { var channelEls = els.find( "*[data-rt-channel]" ); log( 'found ' + channelEls.size() + ' channels' ); channelEls.each( subscribeChannel ); } function subscribeChannel( index, el ) { el = $( el ); var pusher = getPusher(); var channelName = el.attr( 'data-rt-channel' ); var channel = pusher.subscribe( channelName ); } function find( els, selector ) { var topLevelEls = els.filter( selector ); var childEls = els.find( selector ); return topLevelEls.add( childEls ); }
The ‘find’ function is a utility function to get any elements from an existing collection that match a given selector using
along with any descendants of the elements using We use this function to find any elements marked up to represent channel subscriptions (‘data-rt-channel’ attribute) and for each we then call ‘subscribeChannel’. This function extracts the name of the channel to be subscribed to and uses the value in calling ‘pusher.subscribe( channelName )’ to actually subscribe to the channel.Bind
We then need to find any elements marked up to represent events (‘data-rt-event’ attribute) to be bound to:
function subscribeChannel( index, el ) { el = $( el ); var pusher = getPusher(); var channelName = el.attr( 'data-rt-channel' ); var channel = pusher.subscribe( channelName ); var eventEls = find( el, '*[data-rt-event]' ); log( 'found ' + eventEls.size() + ' events' ); eventEls.each( function( i, el) { bind( el, channel ); } ); } function bind( el, channel ) { el = $( el ); var eventName = el.attr( 'data-rt-event' ); channel.bind( eventName, function( data ) { displayUpdate( el, data ); } ); } function displayUpdate( el, data ) { }
For each event element we find call our own ‘bind’ function which binds to the event on the channel using ‘channel.bind( eventName, eventHandler )’. The event handler function is a small closure which allows us to pass the data update, when received, and the event element to a ‘displayUpdate’ function.
If we run this now we can see from the logging that a connection is being established, we’re finding one channel and subscribing to it, and finding one event to bind to.
jQuery realtime markup finding channel subscription and event binding
Display the update
When the event handler is called we need to find the name of each property on the ‘data’ object (e.g. last, low, high and volume) sent with the update and find any elements that are marked with that name.
function bind( el, channel ) { el = $( el ); var eventName = el.attr( 'data-rt-event' ); channel.bind( eventName, function( data ) { displayUpdate( el, data ); } ); } function displayUpdate( el, data ) { for( var propName in data ) { var value = data[ propName ]; var updateEls = find( el, '*[data-rt-value="' + propName + '"]' ); log( 'found ' + updateEls.size() + ' "' + propName + '" elements to update' ); updateEls.text( value ); } }
We loop over the ‘data’ object and get the name of each property. Once we know the property name (‘propName’) we can find the associated elements and update their text value with the new data value. For now we’re not going to support objects with any kind of hierarchy — we just want one level of key and value pairs.
If you now refresh the page and trigger an event from the Pusher Event Creator the new data will be instantly displayed within the page.
No comments:
Post a Comment