1. Extending Your jQuery Application with AmplifyJS

    Elijah Manor

  2. Who Am I?

    Elijah Manor

    Microsoft Regional Directory, Microsoft ASP.NET MVP, ASPInsider

    Senior Architect and Director of Training at appendTo

    Social Media Junkie

    • elijah.manor@gmail.com
    • http://twitter.com/elijahmanor
    • http://gplus.to/elijahmanor
    • http://elijahmanor.com
  3. Today We Will...

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

    • Show how to extend the application with AmplifyJS

    • Show an easy approach to unit test your code

    • Show a helpful tool to help you quickly prototype your user interface

    • Show how AmplifyJS can reduce risk when your datasource dramatically changes

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

  5. Typical 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();
  6. 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)
  7. 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.

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

  9. 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 );
  10. 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();
  11. 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.

  12. 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
    amplify.store( "contact", { 
      firstName: "John", lastName: "Smith" 
    });
  13. 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
  14. 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" ),
                thisTime = new Date().getTime();
    
            if ( !news || ( thisTime - news.timeStamp > 5000 ) ) {
                lastTime = thisTime;
                $.ajax({
                    url: "http://api.ihackernews.com/page?format=jsonp",
                    dataType: "jsonp",
                    success: function( data, textStatus, jqXHR ) {
                        data.timeStamp = thisTime;
                        amplify.store( "news", data );
                        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();
  15. 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.

  16. 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     
    });
  17. 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 );
        });
  18. 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 ); 
        }
    });
  19. Extending with Request ^

    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 );
            });    
    
            amplify.request.define( "getNews", "ajax", {
                url: "http://api.ihackernews.com/page?format=jsonp", 
                dataType: "jsonp",
                cache: 3000
            });  
        };
    
        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();
  20. 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
  21. 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" }
    		]
    	});
    });
  22. 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"                
                }
            ]
        });
    });
  23. 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 );
            });
    
        });
    
    });
  24. Prototyping with MockJSON

    mockJSON is a jQuery Plugin that has a valuable templating mechnaism that lets you quickly build up complex set of objects that can be used to quickly prototype your User Interface.

  25. MockJSON: Built-in Types

    // Numbers
    { "age|0-99" : 0 } // { "age" : 42 }
    
    // Boolean
    { "married|0-1" : true } // { "married": false }
    
    // Strings
    { "name" : "@LAST_NAME, @MALE_FIRST_NAME" } // { "name" : "Hall, Kevin" }
    { "initials|1-5" : "@LETTER_UPPER." } // { "initials": "V.M.J." }
    
    // Arrays
    { "contacts|0-3" : [ { "Name" : "@FEMALE_FIRST_NAME" } ] } 
    // { "contacts": [{ Name: "Laura" }, { Name: "Ruth" }] }
  26. MockJSON: Custom Types

    // Adding the @US_STATE keyword
    $.mockJSON.data.US_STATE = [
        "Alabama", "Alaska", ... , "Wisconsin", "Wyoming"
    ];
    { "state" : "@US_STATE" } // { "state": "Alaska" }
  27. MockJSON Template Syntax

    $.mockJSON.generateFromTemplate({
        "contacts|2-4": [{
            "firstName": "@MALE_FIRST_NAME",
            "lastName": "@LAST_NAME",
            "city": "@CITY_NAME",
            "state": "@STATE_ABBREVIATION"
        }]
    });
  28. MockJSON'ing Hacker News ^

    amplify.request.define( "getNews", function( settings ) {
        settings.success($.mockJSON.generateFromTemplate({
            "status": true,
            "items|20-30": [
                {
                    "id|+1" : 0,
                    "commentCount|2-15": 0,
                    "points|20-80": 0,
                    "postedAgo": "@DATE_MM/@DATE_DD/@DATE_YYYY",
                    "postedBy": "@MALE_FIRST_NAME @LAST_NAME",
                    "title": "@LOREM_IPSUM",
                    "url": "@URL"
                }
            ]
        }));
    });
    
    $.mockJSON.data.URL = [
        "http://google.com", "http://yahoo.com", "http://microsoft.com", "http://apple.com"
    ];
  29. 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.

  30. 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
  31. 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 ... */
    
    };
  32. Custom Request Decoder

    Since the data coming back from YQL looks different from what the web app was retieving from Hacker News previously we will need to map the results to the format our app is expecting.

    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"
    				}
    			]
    		}
    	}
    }

    ...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"
    }
  33. 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
            };
        });   
    }   
  34. 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
    });  
  35. Questions?

    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
    • http://elijahmanor.com