This presentation is an HTML5 website
Press → key to advance.
Having issues seeing the presentation? Read the disclaimer
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.
Taking an existing web application and slowing adding 3 AmplifyJS components
AmplifyJS is a set of components designed to solve common web application problems with a simplistic API.
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
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.
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();
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.
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.
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 tocontext: What this will be when the callback is invokedcallback: Function to invoke when the message is publishedpriority: Priority relative to other subscriptions for the same message
amplify.subscribe( "contactUpdated",
function( contact ) {
console.log( contact.firstName ); // John
}, 5 );
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();
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.
amplify.store( string key, mixed value [, hash options ] )
key: identifier for the value being storevalue: The value to store. The value can be anything that can be serialized as JSONoptions (optional): A set of key/value paris that relate to settings for storing the value (expires)
amplify.store( "contact", {
firstName: "John", lastName: "Smith"
});
amplify.store( string key )
var contact = ampilfy.store( "contact" ); contact.firstName; // John
amplify.store(); var store = amplify.store(); store.contact.firstName; // John
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();
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.
// Define a Request amplify.request.define(string resourceId, string requestType [, hash settings ])
resourceId: Identifier string for the resourcerequestType: Type of data retrieval method from the server.settings (optional): Set of key/value pairs
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
});
// 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 resourcecallback (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 );
});
// Request with Hash Settings amplify.request( hash settings )
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 );
}
});
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();
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...
Whatever amplify.request.define is defined last will be used.
// Define a Mock Request amplify.request.define( string resourceId, function response )
amplify.request.define( "getContactDetails", function( settings ) {
settings.success({
status: "success",
contacts: [
{ firstName: "Mike" },
{ firstName: "Jonathan" },
{ firstName: "Scott" }
]
});
});
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"
}
]
});
});
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 );
});
});
});
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.
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...
The above select query translates into the following encoded URL with all the necessary parameters for the query
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 ... */
};
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"
}
]
}
}
}
...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"
}
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
};
});
}
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
});