Loading...

This presentation is an HTML5 website

Press key to advance.

Having issues seeing the presentation? Read the disclaimer

Slides controls
  • and to move around.
  • Ctrl/Command and + or - to zoom in and out.
  • S to view page source.
  • T to change the theme.
  • H to toggle syntax highlight.
  • N to toggle speaker notes.
  • 3 to toggle 3D effect.
  • 0 to toggle help.
Slide Notes

When you see the '✎' in the title of the slide you can click on it to launch jsFiddle to run the associated code example.

Once you launch the jsFiddle you can remove the embedded/... from the URL if you want to tweak the results and play with the code.

  • http://jsfiddle.net/ng4Qt/embedded/js,html,css,result/presentation/
  • c
  • http://jsfiddle.net/ng4Qt

Extending Your jQuery App with AmplifyJS

by Elijah Manor

Taking an existing web application and slowing adding 3 AmplifyJS components

AmplifyJS JavaScript Library

AmplifyJS is a set of components designed to solve common web application problems with a simplistic API.

  • Client Side Component Communication (publish/subscribe)
  • Client Side Browser & Mobile Device Storage (store)
  • Ajax Request Managment (request)
Today We Will...

Show an example of a typical web application that you may encounter

Show how to extend the application with AmplifyJS

Show how to easily mock web service calls to let you continue developing the font-end

Show an easy approach to unit test your code

Front-End Challenge

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.

Hacker News Web Application
var hackerNews = (function( $, undefined ) {
    var pub = {};

    pub.init = function() {
        $( "#btnRefresh" ).live( "click", function() {
            pub.getAndDisplayNews();
        });        
    };

    pub.getAndDisplayNews = function() {
        $.mobile.pageLoading();  

        getNews( function( data ) {
            displayNews( data );    

            $.mobile.pageLoading( true ); 
        });
    };

    function getNews( callback ) { 
        $.ajax({
            url: "http://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" );

        newsList.empty();
        $( "#newsItem" ).tmpl( news.items ).appendTo( newsList );
        newsList.listview( "refresh" );        
        $( "#itemCount" ).text( news.items.length );        
    }

    return pub;
}( jQuery ));

hackerNews.init();
hackerNews.getAndDisplayNews();

AmplifyJS Pub / Sub Component

AmplifyJS Pub / Sub Component

Client Side Component Communication - amplify.publish/subscribe provides a clean, performant API for component to component communication.

Note: You could use jQuery Custom Events to simulate this interaction, but it is tightly bound to a DOM element and the syntax isn't as graceful when all you want is just a pub/sub component.

AmplifyJS Pub / Sub API

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
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.

AmplifyJS Pub / Sub API…

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
amplify.subscribe( "contactUpdated", 
  function( contact ) {
    console.log( contact.firstName ); // John
  }, 5 );



Extending with Pub / Sub
var hackerNews = (function( $, undefined ) {
    var pub = {};

    pub.init = function() {
        $( "#btnRefresh" ).live( "click", function() {
            pub.getAndDisplayNews();
        });

        amplify.subscribe( "news.updated", function( news ) {
            displayNews( news );
        });

        amplify.subscribe( "news.updated", function( news ) {
            displayItemCount( news.items.length );
        });
    };

    pub.getAndDisplayNews = function() {
        $.mobile.pageLoading();  

        getNews( function( data ) {
            amplify.publish( "news.updated", data );

            $.mobile.pageLoading( true ); 
        });
    };

    function getNews( callback ) { 
        $.ajax({
            url: "http://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" );

        newsList.empty();
        $( "#newsItem" ).tmpl( news.items ).appendTo( newsList );
        newsList.listview( "refresh" );        
    }

    function displayItemCount( count ) {
        $( "#itemCount" ).text( count );    
    }

    return pub;
}( jQuery ));

hackerNews.init();
hackerNews.getAndDisplayNews();

AmplifyJS Store Component

AmplifyJS Store Component…

Client Side Browser & Mobile Device Storage

amplify.store takes the confusion out of HTML5 localStorage. It doesn't get simpler than using amplify.store( key, data )! It even works flawlessly on mobile devices.

amplify.store supports IE 5+, Firefox 2+, Safari 4+, Chrome, Opera 10.5+, iPhone 2+, Android 2+ and provides a consistent API to handle storage cross-browser.













AmplifyJS Store 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 (expires)
amplify.store( "contact", { 
  firstName: "John", lastName: "Smith" 
});
AmplifyJS Store API

Get a Value from Storage

amplify.store( string key )
  • key: Identifier for the value stored
var contact = ampilfy.store( "contact" );
contact.firstName; // John

Get a Hash of All Stored Values

amplify.store();

var store = amplify.store();
store.contact.firstName; // John
Extending with Store
var hackerNews = (function( $, undefined ) {
    var pub = {};

    pub.init = function() {
        $( "#btnRefresh" ).live( "click", function() {
            pub.getAndDisplayNews();
        });

        amplify.subscribe( "news.updated", function( news ) {
            displayNews( news );
        });

        amplify.subscribe( "news.updated", function( news ) {
            displayItemCount( news.items.length );
        });              
    };

    pub.getAndDisplayNews = function() {
        $.mobile.pageLoading();  

        getNews( function( data ) {
            amplify.publish( "news.updated", data );            
            $.mobile.pageLoading( true ); 
        });
    };

    function getNews( callback ) {
        var news = amplify.store( "news" );

        if ( !news ) {
            $.ajax({
                url: "http://api.ihackernews.com/page?format=jsonp",
                dataType: "jsonp",
                success: function( data, textStatus, jqXHR ) {
                    amplify.store( "news", data, { expires: 5000 } );
                    callback && callback( data );
                },
                error: function( jqXHR, textStatus, errorThrown ) {
                    console.log( textStatus + ": " + errorThrown );
                }
            });
        } else {
            callback && callback( news );
        }
    }

    function displayNews( news ) {
        var newsList = $( "#hackerNews" ).find( ".newsList" );

        newsList.empty();
        $( "#newsItem" ).tmpl( news.items ).appendTo( newsList );
        newsList.listview( "refresh" );        
    }

    function displayItemCount( count ) {
        $( "#itemCount" ).text( count );    
    }

    return pub;
}( jQuery ));

hackerNews.init();
hackerNews.getAndDisplayNews();

AmplifyJS Request Component

AmplifyJS 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 request of the data.

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.

AmplifyJS Request Define API
// Define a Request
amplify.request.define(string resourceId, string requestType [, hash settings ]) 
  • resourceId: Identifier string for the resource
  • requestType: Type of data retrieval method from the server.
  • settings (optional): Set of key/value pairs
    • Any settings found in jQuery.ajax()
    • cache: Different caching algorithms (boolean, number, string)
    • decoder: Parse response before calling success or error callback
amplify.request.define( "getContactDetails", "ajax", {
    //AmplifyJS 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     
});
AmplifyJS Request Simple API
// 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
amplify.request( "getContactDetails", 
    { id: 4 }, // Resolve URL to "/Contact/Details/4"
    function( data ) {
        console.log( data );
    });
AmplifyJS Request Hash API
// 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
amplify.request({ 
    resourceId: "getContactDetails",
    data: { id: 4 }, // Resolve URL to "/Contact/Details/4"
    success: function( data ) { console.log( data ); },
    error: function( message, level ) { 
      console.log( level + ": " + message ); 
    }
});
Extending with Request
amplify.request.define( "getNews", "ajax", {
    url: "http://api.ihackernews.com/page?format=jsonp", 
    dataType: "jsonp",
    cache: 3000
});  

var hackerNews = (function( $, undefined ) {
    var pub = {};

    pub.init = function() {
        $( "#btnRefresh" ).live( "click", function() {
            pub.getAndDisplayNews();
        });

        amplify.subscribe( "news.updated", function( news ) {
            displayNews( news );
        });

        amplify.subscribe( "news.updated", function( news ) {
            displayItemCount( news.items.length );
        });    
    };

    pub.getAndDisplayNews = function() {
        $.mobile.pageLoading();  

        getNews( function( data ) {
            amplify.publish( "news.updated", data );            
            $.mobile.pageLoading( true ); 
        });
    };

    function getNews( callback ) {
        amplify.request({
            resourceId: "getNews",
            success: function( data ) {
                if ( callback ) callback ( data );
            },
            error: function( message, level ) {
                console.log( level + ": " + message );
            }
        });        
    }

    function displayNews( news ) {
        var newsList = $( "#hackerNews" ).find( ".newsList" );

        newsList.empty();
        $( "#newsItem" ).tmpl( news.items ).appendTo( newsList );
        newsList.listview( "refresh" );        
    }

    function displayItemCount( count ) {
        $( "#itemCount" ).text( count );    
    }

    return pub;
}( jQuery ));

hackerNews.init();
hackerNews.getAndDisplayNews();
Mocking with AmplifyJS Require

It seems that even those that are aware of the AmplifyJS library aren't aware that you can easily you it to mock the response from your server. Mocking the User Interface interactions can be very beneficial for numerous reasons.

Let's say that you are working on a client and the back-end RESTful or Web Services are...

  • Not yet developed
  • Extremely unstable
  • Not accessible due to VPN or cross-domain concerns
Mocking with AmplifyJS Require API

Whatever amplify.request.define is defined last will be used.

// Define a Mock Request
amplify.request.define( string resourceId, function response ) 
  • resourceId: Identifier string for the resource (should match previous string)
  • response: Function that will return either a success or failure response
amplify.request.define( "getContactDetails", function( settings ) {
  settings.success({
    status: "success",
    contacts: [
      { firstName: "Mike" },
      { firstName: "Jonathan" },
      { firstName: "Scott" }
    ]
  });
});
Mocking Hacker News
amplify.request.define( "getNews", function( settings ) {
    settings.success({
        status: "success",
        items: [
            {
                commentCount: 1,
                id: 1,
                points: 1,
                postedAgo: "1 hours ago",
                postedBy: "jkeesh",
                title: "1 Title",
                url: "http://1.com"
            },
            {
                commentCount: 2,
                id: 2,
                points: 2,
                postedAgo: "2 hours ago",
                postedBy: "jkeesh",
                title: "2 Title",
                url: "http://2.com"                
            },
            {
                commentCount: 3,
                id: 3,
                points: 3,
                postedAgo: "3 hours ago",
                postedBy: "jkeesh",
                title: "3 Title",
                url: "http://3.com"                
            },
            {
                commentCount: 4,
                id: 4,
                points: 4,
                postedAgo: "1 hours ago",
                postedBy: "jkeesh",
                title: "4 Title",
                url: "http://4.com"                
            },
            {
                commentCount: 5,
                id: 5,
                points: 5,
                postedAgo: "5 hours ago",
                postedBy: "jkeesh",
                title: "5 Title",
                url: "http://5.com"                
            }
        ]
    });
});
Mocking with Unit Tests
pavlov.specify.globalApi = true;
pavlov.specify( "News Aggregator", function() {

    describe( "User Interactions", function() {

        var button = $( "#btnRefresh" ),
            isUpdated = false, news,
            newsUpdatedCallback = function( data ) {
                isUpdated = true;
                news = data;
            };

        before( function() {
            isUpdated = false;

            hackerNews.init();

            amplify.request.define( "getNews", function( settings ) {
                settings.success({
                    status: "success",
                    items: [
                        { commentCount: 5, id: 1, points: 50, postedAgo: "1 hour ago", postedBy: "davidedicillo", title: "Five four three...", url: "http://techcrunch.com/5" }, 
                        { commentCount: 4, id: 2, points: 40, postedAgo: "2 hour ago", postedBy: "davidedicillo", title: "Four three two...", url: "http://techcrunch.com/4" }, 
                        { commentCount: 3, id: 3, points: 30, postedAgo: "3 hour ago", postedBy: "davidedicillo", title: "Three two one...", url: "http://techcrunch.com/3" }, 
                        { commentCount: 2, id: 4, points: 20, postedAgo: "4 hour ago", postedBy: "davidedicillo", title: "Two, one, zero...", url: "http://techcrunch.com/2" }, 
                        { commentCount: 1, id: 5, points: 10, postedAgo: "5 hour ago", postedBy: "davidedicillo", title: "One, zero, negative one...", url: "http://techcrunch.com/1" }
                    ]
                });
            });          

            amplify.subscribe( "news.updated", newsUpdatedCallback );
        });

        after( function() {
            amplify.unsubscribe( newsUpdatedCallback );
        });

        it( "should publish a news updated message", function() {
            button.trigger( "click" );
            stop();

            setTimeout( function() {
                start();
                assert( isUpdated ).isTrue();
            }, 2000 );
        });

        it( "should return 5 news items", function() {
            button.trigger( "click" );
            stop();

            setTimeout( function() {
                start();
                assert( news ).isDefined();
                assert( news.items.length ).isEqualTo(5);
            }, 2000 );
        });

    });

});
Warning: Major Refactor Ahead!

What if our back-end datasource totally change!?! What if instead of always getting a Hacker News feed that our boss came and said, "Why don't you use your same code to pull in any news feed?". How would you go about doing that?

AmplifyJS Request is architected in such a way that we can use Uncle Bob's SOLID principle of OCP (The Open Closed Principle) stating that we should be able to extend the behavior, without modifying it.

New DataSource: YQL

In YQL you an query a RSS or Atom feed and it will proxy the results and make it accessible for you via XML or JSON.

The following is an example of a query you might use...

  • select * from feed where url='http://feeds2.feedburner.com/readwriteweb/hack' limit 10

The above select query translates into the following encoded URL with all the necessary parameters for the query

  • http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20feed%20where%20url%3D'http%3A%2F%2Ffeeds2.feedburner.com%2Freadwriteweb%2Fhack'%20limit%2010&format=json&callback=cbfunc
Custom Request Type

In order to start creating our new RSS request type, I usually start by copying the native ajax type that is shipped with AmplifyJS Request and then I tweak out any changes that I need.

Here is a snippet of what this might look like...

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

    return function( settings, request ) {        
        var url = 
            stringFormat( "http://query.yahooapis.com/v1/public/yql?q={0}&format=json",
                encodeURIComponent( stringFormat( 
                    "select * from feed where url='{0}' LIMIT {1};", 
                    typeSettings.url, typeSettings.itemsToRetrieve ) ) ),

    /* ... more code ... */

};
Custom Request Decoder

Since the YQL data looks different than the data from Hacker News we will need to map the results from one to the other.

To do this, we will define a new decoder that will take this format...

{
  query: {
    count: 10,
    created: "2011-08-18T04:32:16Z",
    lang: "en-US",
    results: {
      item: [
        {
          title: "This is a test title",
          description: "This is a test description",
          link: "http://google.com",
          guid: { isPermaLink: "true", content: "" },
          category: "TEST",
          pubDate: "2011-08-18T04:32:16Z",
          author: "Spider-Manor",
          origLink: "http://google.com"
        }
      ]
    }
  }
}
Custom Request Decoder

...and map it to the following format that our web applicaiton is expecting...

{
  cachedOnUTC: "/Date(1313643183975)/"
  items: [
    {
      commentCount: 20
      id: 2897740
      points: 81
      postedAgo: "2 hours ago"
      postedBy: "jkeesh"
      title: "Who Does Facebook Think You Are Searching For?"
      url: "http://thekeesh.com/2011/08/who-does-facebook-think-you-are-searching-for/"
    }
  ]
  nextId: "m97628CrNN"
  version: "1.0"
}
Custom Request Decoder
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
        };
    });   
}   
Reusable Generic News Reader

After creating your custom type and decoder you can now change your amplify.request.define to look like the following and you don't have to change the code that actually makes the request!

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

Elijah Manor

  • ASP.NET MVP, ASPInsider
  • Senior Architect at appendTo

Referenced Scripts

  • AmplifyJS - http://bit.ly/amplifyjs
  • mockJSON - http://bit.ly/mockjson

Contact Information

  • elijah.manor@gmail.com
  • http://twitter.com/elijahmanor
  • http://gplus.to/elijahmanor