Extending Your jQuery Application with Amplify.js

Elijah Manor, Andrew Wirick | April 27, 2011

As your application grows you will find that it is important to abstract the various pieces of your implementation to help you easily extend the functionality and make it less painful to maintain.

appendTo has recently released a set of JavaScript components that can assist with common problems in front-end development. The library was developed to "Amplify" your current jQuery skills and provide some missing components that are important for developing scalable web applications.

Sample Application

In order to exercise these components it is sometimes easier to see how they are used in a sample application, so I'll be creating a simple aggregator of Hacker News. We will first look at a working example of this application that works, but is tightly coupled and brittle. Then, as I will introduce each component, I will slowly enhance the application to use Amplify.

We will use the following HTML in all the examples that will be used in the rest of this article. 

<div data-role="page" id="hackerNews">
     
  <div data-role="header">
    <a id="btnRefresh" href="#" 
      data-icon="refresh">Refresh</a>
      <h1>Hacker News &nbsp;
        <span id="itemCount" class="count ...">0</span>
      </h1>
  </div>
     
  <div id="content" data-role="content">
    <ol class="newsList" data-role="listview"></ol>
  </div>
    
</div>
<script id="newsItem" type="text/x-jquery-tmpl">
  <li class="newsItem">
    <h3><a href="${url}">${title}</a></h3>
    <p class="subItem">
      <strong>${postedAgo} by ${postedBy} </strong>
    </p>
    <div class="ui-li-aside">
      <p><strong>${points} points</strong></p>
      <p>${commentCount} comments</p>
    </div>
  </li>
</script>

The above HTML represents a jQuery Mobile page. The markup for jQuery Mobile is very clean and it is extremly easy to put together a nice looking web application. For more information on jQuery Mobile development there are various detailed docs and demos you can view, but for the purposes of this article you don't need to know the details of jQuery Mobile. Our sample web application has a header and content section. Inside the content is a ListView jQuery UI widget where the news items will be displayed. We will be using the jQuery Template Plugin to generate our news items once we get the data from the Hacker News server.

The following application code respresents a "typical" application that you might write or come across on the web or at work. 

var hackerNews = (function( $, undefined ) {
    var pub = {};
 
    pub.init = function() {
        //Refresh news when btnRefresh is clicked
        $( "#btnRefresh" ).live( "click", function() {
            pub.getAndDisplayNews();
        });        
    };
     
    pub.getAndDisplayNews = function() {
        //Starting loading animation
        $.mobile.pageLoading();  
         
        //Get news and add success callback using then
        getNews( function( data ) {
            //Display the news items in the user interface
            displayNews( data );    
             
            //Stop loading animation on success
            $.mobile.pageLoading( true ); 
        });
    };
     
    function getNews( callback ) { 
        //Request for news from jQuery ajax
        $.ajax({
            url: "https://api.ihackernews.com/page?format=jsonp",
            dataType: "jsonp",
            success: function( data, textStatus, jqXHR ) {
                if ( callback ) callback ( data );
            },
            error: function( jqXHR, textStatus, errorThrown ) {
                console.log( textStatus + ": " + errorThrown );
            }
        });                
    }
     
    function displayNews( news ) {
        var newsList = $( "#hackerNews" ).find( ".newsList" );
         
        //Empty current list
        newsList.empty();
         
        //Use template to create items & add to list
        $( "#newsItem" ).tmpl( news.items ).appendTo( newsList );
         
        //Call the listview jQuery UI Widget after adding 
        //items to the list allowing correct rendering
        newsList.listview( "refresh" );        
 
        //Update the number of news items
        $( "#itemCount" ).text( news.items.length );        
    }
     
    return pub;
}( jQuery ));
 
hackerNews.init();
hackerNews.getAndDisplayNews();

You can view, run, and edit the above source code from jsFiddle.

There are many good things that  the code has going on for it such as protecting the global namespace and  the use public and private methods, but beyond that there are several  areas were it can be improved for future enhancement and maintenance. We will look at these areas for improvement in the rest of the article. 

Publishing and Subscribing to Topics with Amplfiy

The Amplify core library provides two methods (amplify.publish and amplify.subscribe). Amplify provides methods to facilitate the Publish and Subscribe messaging pattern in your front-end application. The idea is that someone is broadcasting one or more messages (publishing) and someone else is listening to one or more messages (subscribing). By separating your logic out like this it allows for loose coupling of your components, which results in less brittle and more reusable code.

It is possible to implement the publish and subscribe model by using jQuery custom events, however, the Amplify pub/sub component provides a slightly cleaner interface, prevents collisions between custom events and method names, and allows a priority to your messages.

Publish API

The basic syntax to the ampliy.publish method is the following:

Publish a Topic

amplify.publish( string topic, ... )
  • topic: The name of the message to publish
  • ... : Any additional parameters that will be passed to the subscriptions

Example Usage

amplify.publish( "contactUpdated", { 
  firstName: "John", lastName: "Smith" 
});

The publish will return a boolean indicating whether any subscribers returned false. If a subscriber returns false, then it prevents any additional subscriptions from being invoked.

Subscribe API

The basic parameters for amplify.subscribe are simple (a topic and a callback function), but the method is also "overloaded" to give you more control if you need it. 

Subscribe to a Topic

amplify.subscribe( string topic, function callback )
amplify.subscribe( string topic, object context, function callback )
amplify.subscribe( string topic, function callback, number priority )
amplify.subscribe( string topic, object context, function callback, number priority )
  • topic: Name of the message to subscribe to
  • context: What this will be when the callback is invoked
  • callback: Function to invoke when the message is published
  • priority: Priority relative to other subscriptions for the same message. Lower values have higher priority. Default is 10.

Example Usage

amplify.subscribe( "contactUpdated", 
  function( contact ) {
    console.log( contact.firstName ); // John
  }, 5 );

Sample Application

Let's see how I can integrate pub/sub into our sample application. The following code will use the amplify.publish and amplify.subscribe methods to separate the server data retrieval from the actual user interface updates. I am using the Revealing Module Pattern in order to prevent the number of objects in the global namespace and to introduce public and private methods.

var hackerNews = (function( $, undefined ) {
  var pub = {};
 
  pub.init = function() {
    //Refresh news when btnRefresh is clicked
    $( "#btnRefresh" ).live( "click", function() {
      pub.getAndDisplayNews();
    });
         
    //When news updated, display items in list
    amplify.subscribe( "news.updated", 
      function( news ) {
        displayNews( news );
      });
 
    //When news updated, then set item count
    amplify.subscribe( "news.updated", 
      function( news ) {
        displayItemCount( news.items.length );
      });              
  };
     
  pub.getAndDisplayNews = function() {
    //Starting loading animation
    $.mobile.pageLoading();  
         
    //Get news and add success callback using then
    getNews( function( data ) {
      //Publish that news that been updated & allow
      //the 2 subscribers to update the UI content
      amplify.publish( "news.updated", data );            
             
      //Stop loading animation on success
      $.mobile.pageLoading( true ); 
    });
  };
     
  function getNews( callback ) { 
    //Request for news from jQuery ajax
    $.ajax({
      url: "https://api.ihackernews.com/page" + 
        "?format=jsonp",
      dataType: "jsonp",
      success: function( data, textStatus, jqXHR ) {
        if ( callback ) callback ( data );
      },
      error: function( jqXHR, textStatus, error ) {
        console.log( textStatus + ": " + error );
      }
    });                
  }
     
  function displayNews( news ) {
    var newsList = 
      $( "#hackerNews" ).find( ".newsList" );
         
    //Empty current list
    newsList.empty();
         
    //Use template to create items & add to list
    $( "#newsItem" ).tmpl( news.items )
      .appendTo( newsList );
         
    //Call the listview jQuery UI Widget after adding 
    //items to the list allowing correct rendering
    newsList.listview( "refresh" );        
  }
 
  function displayItemCount( count ) {
    $( "#itemCount" ).text( count );    
  }
     
  return pub;
}( jQuery ));
 
hackerNews.init();
hackerNews.getAndDisplayNews();

You can view, run, and edit the above source code from jsFiddle.

The main entry point for the above sample application is the getAndDisplayNews method. This method calls to get the data from the server (using the $.ajax() jQuery method) and then publishes the data as a news.updated message. There are two subscribers to that message that independently update the user interface with display methods (displayNews and displayItemCount). 

By splitting out the user interface updates as various subscriptions it allows each listener to specialize its code to serve one purpose and to do it well. The subscribers don't know anything about each other. Each of the subscribers is only concerned with its own responsibility. We've achieved separation of concerns.

Benefits

  • The component allows for an abstraction layer that enables loose coupling of your components
  • The API provides the ability to determine priority of messages that are broadcasted
  • Allows you to code your functions with a specific purpose without intimate knowledge of other parts of the system

Request Component

The amplify.request component sets out to make data retrieval more maintainable. It does this by separating the definition of a request from the actual implementation of requesting the data. The definition of the request is only responsible for deciding the caching strategy, server location, data type, decoders, headers, etc. While the actual requestor code only needs to know a request ID. This allows for a data layer which can make your application much easier to test and more agile to change.

Request Define API

The role of amplify.request.define is to allow you to focus on only defining the implementation of retrieving data. The API for amplify.request.define can be very simple, but also has numerous optional parameters that you can leverage, including some that you can provide custom implementations of your own.

Define a Request

amplify.request.define( string resourceId, string requestType [, hash settings ] )
  • resourceId: Identifier string for the resource

  • requestType: The type of data retrieval method from the server. There is currently one built in requestType (ajax), but you can also define your own.

  • settings (optional): A set of key/value pairs that relate to the server communication technology.

    • Any settings found in jQuery.ajax()

    • cache: Providing different caching algorithms for the request and response

      • boolean: Cache the data in-memory for the remainder of the page load

      • number: Cache the data in-memory for the specified number of milliseconds

      • string: Persistent client-side cache using amplify.store

        • "persist": Will use the first available storage type as default
        • "localStorage", "sessionStorage", etc. ... 
    • decoder: A way to parse an ajax response before calling the success or error callback. There is currently one built in decoder (jSend), but you can also define your own. By default no decoder is used unless you set it.

      • "jsend": A specification on how JSON responses should be formatted

Example Usage

amplify.request.define( "getContactDetails", "ajax", {
  //Amplify will replace {id} with data passed to it
  url: "/Contact/Details/{id}", 
  dataType: "json",
  type: "GET", 
  //Response will be cached for 15 seconds
  cache: 15000     
});

Request API

Once you've defined your request, then the next step would be to go ahead and call the amplify.request method. Thankfully, most of the hard part was defining the definition of the request, so calling amplify.request is fairly straightforward.

Simplified Request

amplify.request( string resourceId 
  [, hash data [, function callback ]] )
  • resourceId: Identifier string for the resource
  • data (optional): an object literal of data to be sent to the resource
  • callback (optional): a function to call once the resource has been retrieved

Example Usage

amplify.request( "getContactDetails", 
  //Amplify will resolve url to "/Contact/Details/4"
  { id: 4 }, 
  function( data ) {
    console.log( data );
  });

Request with Hash Settings

amplify.request( hash settings )
  • settings
    • resourceId: Identifier string for the resource
    • data (optional): Data associated with the request
    • success (optional): Function to invoke on success
    • error (optional): Function to invoke on error

Example Usage

amplify.request({ 
  resourceId: "getContactDetails",
  //Amplify will resolve url to "/Contact/Details/4"
  data: { id: 4 }, 
  success: function( data ) {
    console.log( data );
  },
  error: function( message, level ) {
    console.log( level + ": " + message );
  }
});

Sample Application

Now we will update the sample application from above and swap out the call to $.ajax() to use amplify.request instead.

var hackerNews = (function( $, undefined ) {
  var pub = {};
 
  pub.init = function() {
    //...
 
    //Define getNews AJAX JSONP request in Amplify
    amplify.request.define( "getNews", "ajax", {
      url: "https://api.ihackernews.com/page" + 
        "?format=jsonp", 
      dataType: "jsonp",
      cache: 30000
    });                
  };
     
  //...
     
  function getNews( callback ) { 
    //Request for news from Amplify        
    amplify.request({
      resourceId: "getNews",
      success: function( data ) {
        if ( callback ) callback ( data );
      },
      error: function( message, level ) {
        console.log( level + ": " + message );
      }
    });        
  }
     
  //...
     
  return pub;
}( jQuery ));
 
hackerNews.init();
hackerNews.getAndDisplayNews();

You can view, run, and edit the above source code from jsFiddle.

We first defined what the request will look like and placed that in our init method. We define the request as getNews, use the built in ajax type, and then pass some options which include common $.ajax() settings such as url and dataType, but also includes a amplify.request specific cache setting where we will cache the response of the request for 30000 milliseconds (30 seconds).

After defining our request with amplify.request.define, we then swapped out the $.ajax() code that was in the getNews method and replaced it with amplify.request referencing the resourceId we previously defined, a success callback, and an error callback method.

With caching now on when the user clicks on the Refresh button there will be a 30 second period where no actual ajax request is being made to the server. You can verify this using your web developer tools in your browser. Once the cache has been cleared after 30 seconds, then the real request will proceed to the server and return fresh results that will be in-turn cached for another 30 seconds.

By making these two small changes the application still works as it did before and we didn't have to change any other code. The power of Amplify is in having the definition adapt with changes while leaving the request functionality untouched. Just think what might happen if the backend switched to web sockets instead or instead of JSON being returned it was XML. Towards the end of this article we will take another look at the power of making this abstraction.

Benefits

  • Separate out the definition from a request from the actual request for data from the server
  • Ability to provide alternative caching and decoding implementations

Storage Component

The next component of Amplify is the amplify.store. Essentially, it is an abstraction around the various forms of persistent client-side storage such as localStorage, sessionStorage, globalStorage, and userData.

amplify.store uses feature detection to determine which storage mechanism is right for your application. By doing so it is able to support IE 5+, Firefox 2+, Safari 4+, Chrome, Opera 10.5+, iPhone 2+, Android 2+ and provides a consistent API to handle storage cross-browser. If for some reason you need more control over which storage mechanism is used, you also have the ability in amplify.store to specify which one you would like to use.

In addition, amplify.store automatically handles any JSON serialization needed when reading or writing JavaScript objects to and from storage.

Storage API

Set a Value to Storage

amplify.store( string key, mixed value 
  [, hash options ] )
  • key: identifier for the value being store
  • value: The value to store. The value can be anything that can be serialized as JSON
  • options (optional): A set of key/value paris that relate to settings for storing the value

Example Usage

amplify.store( "contact", { 
  firstName: "John", lastName: "Smith" 
});

Get a Value from Storage

amplify.store( string key )
  • key: Identifier for the value stored

Example Usage

var contact = ampilfy.store( "contact" );
contact.firstName; // John

Get a Hash of All Stored Values

amplify.store();

Example Usage

var store = amplify.store();
store.contact.firstName; // John

Note: Instead of letting amplify.store determine the storage technique to use, you can also specify a certain type of storage by using one of the following methods instead: localStorage, sessionStorage, globalStorage, userData, or memory. Each of these methods can be used in the same way as the amplify.store method shown above. For more information you can check out the amplify.store documentation online.  

Sample Application

In order to show amplify.store in action I've updated the Refresh button to include a span element representing how many times the button has been clicked. We will keep track of the number of clicks using amplify.store. 

<div data-role="page" id="hackerNews">

  <div data-role="header">
    <a id="btnRefresh" href="#" data-icon="refresh">
      Refreshed: <span id="refreshedCount"></span>
    </a>
    <h1>Hacker News &nbsp;
      <span id="itemCount" class="count ...">0</span>
    </h1>
  </div>
   
  <!-- ... -->
</div>

The following code are the new snippets I've added to the sample application to utilize the amplify.store component.

var hackerNews = (function( $, undefined ) {
  var pub = {};
 
  pub.init = function() {
    //...
 
    amplify.subscribe( "news.updated", 
      function( news ) {
        var count = amplify.store( "refreshedCount" ) || 0;    
        amplify.store( "refreshedCount", ++count );
        displayRefreshedCount( count );
      });
    };
 
  //...
 
  function displayRefreshedCount( count ) {        
    $( "#refreshedCount" ).text( count );
  }
 
  return pub;
}( jQuery ));
 
hackerNews.init()
hackerNews.getAndDisplayNews();

You can view, run, and edit the above source code from jsFiddle.

I added another subscriber to the news.updated message and when the callback function is invoked I pull from amplify.storage to see if refreshedCount exists, increment the value, set the new value back to amplify.storage, and then update the user interface appropriately.  

Benefits

  • The component will provide the best client-side type of persistent for your browser. 
  • You can specify a particular client-side storage to use if you'd rather not have amplify.store figure it out for you.
  • It provides auto JSON serialization when reading or writing JavaScript objects from storage.

Making amplify.request Shine

So far, the sample application we have developed has used a standard AJAX request to an API that Hacker News has made available. Thankfully we didn't have to worry about cross-domain issues since the API endpoint supports JSONP requests.

However, what if you wanted to grab a website's RSS feed instead? Due to cross-domain concerns you would have to either make a proxy on your local domain to grab the RSS feed or possibly use some other technique. Even if you were able to get the data back from a proxy, you could imagine how much existing code you would have to modify in order to make our current sample application continue to work. 

The goal of amplify.request is to abstract the layer of implementation from the actual request and response so that you can minimize code changes to your project. 

To show the value of amplify.request let's modify our sample application to pull it's news from an RSS feed. In order to meet this need we will use YQL (Yahoo Query Language) to get around the cross-domain concerns when accessing an RSS feed. To do this properly we will need to define our own type to be used by Amplify (amplify.request.types). Once the data is returned in the response, in order to match the data format that the existing code is expecting we will define our own decoder to be used by Amplify (amplify.request.decoders) before calling the success or error callbacks. By writing these pieces and briefly tweaking our amplify.request.define statement to utilize the new type and decoder, the rest of our code will work fine without any modification. 

This type of coding follows the Open/Close Principle where the software is open for extension, but closed for modification. If your existing code is known to work, then you run a risk of breaking it by modifying it. If you can code in such a way where you are only adding functionality and not changing it, then you are in a lot better shape.

So, the first thing we will do is to create a new amplify.request.types.rss custom type and a new amplify.request.decoders.rssEnvelope decoder. Assuming these have been created, the only change to our existing code would be to update our amplify.request.define to look like the following and the rest of the code will work just as before.

//Define getNews RSS JSONP request using YQL in Amplify Request
amplify.request.define( "getNews", "rss", {
  url: "https://feeds2.feedburner.com/readwriteweb/hack", 
  decoder: "rssEnvelope",
  itemsToRetrieve: 10
});

You can view, run, and edit the above source code from jsFiddle.

Now, there isn't any magic going on. We did have to define the new type and decoder ourself, but the beauty of that is that we can reuse the type and decoder for other projects if we wanted to or possibly create a GitHub project to share your custom extensions with others.

The following code creating the new rss type and rssEnvelope decoder is the missing piece that makes the rest of the application work.

(function( amplify, $, undefined ) {
        
amplify.request.types.rss = function( typeSettings ) {
    typeSettings = $.extend({
        type: "GET",
        dataType: "jsonp",
        itemsToRetrieve: 5
    }, typeSettings );

    return function( settings, request ) {        
        var url =
            stringFormat( "https://query.yahooapis.com/v1/public/yql?q={0}&format=json",
                encodeURIComponent( stringFormat(
                    "select * from feed where url='{0}' LIMIT {1};",
                    typeSettings.url, typeSettings.itemsToRetrieve ) ) );
                
        console.log( "url: " + url );
        $.ajax( $.extend({}, typeSettings, {
            url: url,
            type: typeSettings.type,
            data: settings.data,
            dataType: typeSettings.dataType,
            success: function( data, status, xhr ) {
                settings.success( data, xhr, status );
            },
            error: function( xhr, status, error, data ) {
                settings.error( data, xhr, status );
            },
            beforeSend: function( xhr, ajaxSettings ) {
                var ret = typeSettings.beforeSend ?
                    typeSettings.beforeSend.apply( this, arguments ) : true;
                return ret && amplify.publish( "request.before.ajax",
                    typeSettings, settings, ajaxSettings, xhr );
            }
        }));
    };
};
    
function stringFormat( source ) {
    var formatted = source,
        length = arguments.length - 1, i;

    for ( i = 0; i < length; i++ ){
        formatted = formatted.replace( "{" + i + "}", arguments[i + 1] );
    }

    return formatted;
}

amplify.request.decoders.rssEnvelope =
    function ( data, status, xhr, success, error ) {
        if ( status === "success" ) {
            success ( { items: transformRssData( data ) } );
        } else if ( status === "fail" || status === "error" ) {
            error( data.message, status );
        } else {
            error( data.message , "fatal" );
        }
    };
        
function transformRssData( data ) {    
    if ( !data || !data.query ||
         !data.query.results || !data.query.results.item ) { return false; }
    
    return $.map( data.query.results.item, function(element, index) {
        var date = new Date( element.pubDate );
        return {
            title: element.title,
            url: element.link,
            postedAgo: date.toLocaleDateString() + " " + date.toLocaleTimeString(),
            postedBy: element.author
        };
    });   
}        
    
}( window.amplify = window.amplify || {}, jQuery ));

You can view, run, and edit the above source code from jsFiddle.

The new amplify.request.types.rss we created accepts an RSS url and passed that to the YQL API using a select statement where we define how many items we want returned. You may notice that we can define custom properties to be passed into our new type as well. In our case we introduced the itemsToRetrieve property which gets inserted into the select statement. 

Once the data has been retrieved we need to do a little massaging of the data in order to make it match the output that our requestor expects, which is why we created a new amplify.request.decoders.rssEnvelope. Inside the decoder we map the values coming back from YQL and assign them to properties that our application recognizes. So for instance, the link property from an RSS item maps to the url property that our jQuery Templates expects.

Creating these new type and decoders wasn't very difficult to do. The easiest way to start is to open up the amplify.request source code and copy theirs as a starting point. That along with the online documentation should be good enough to get you going as you create your own.

If you are already planning on using amplify.request, then you might consider creating a custom type that returns a mocked set of data. This can be a very powerful technique while you are prototyping your application or during unit testing. If for some reason you'd rather not using amplify.request, then the $.mockjax jQuery Plugin can assist you to do the very same thing, but you don't get the same abstraction power that you would get when using amplify.request.

Conclusion

I hope you've seen some of the power of the Amplify components. I've found them a joy to work with and it has been nice to have some tools to help loosely couple the various parts of my web applications.

I encourage you to try out Amplify and see if it can meet the needs of your next web application. Feel free to submit any issues to appendTo's Amplify GitHub repository. Also if you have feedback or questions you'd like to ask then you can join the Amplify Google Group. You can also follow @amplifyjs on Twitter for announcements and upcoming events.

 

About the Author

Elijah Manor is a Christian and a family man. He develops at appendTo as a Senior Architect providing corporate jQuery support, training, and consulting. He is an ASP.NET  MVP, ASPInsider, and specializes in ASP.NET MVC and jQuery development. In addition to blogging, tweeting, and speaking he’s also contributed numerous articles to Script Junkie and was the Co-host of The Official jQuery Podcast.

Find Elijah on:

About the Author

Andrew Wirick is a Senior Trainer and Developer for appendTo. When not coding Andrew travels the country teaching jQuery fundamentals through advanced topics with a specific focus on enterprise developers and designers.

Andrew brings years of enterprise specific jQuery experience. He has helped develop enterprise best practices through years of trials within a large company. He has become passionate about open source and jQuery and has become a regular forum contributor. 

When not browsing GitHub for hidden gems, Andrew never passes up the chance to home-brew a beer. He is a craft beer fanatic, loves too cook, and enjoys all of his hobbies with his wonderful wife.

Find Andrew on: