Wednesday 6 June 2007

Handling JSON responses on the client side

In an previous article on this blog called "Teaching the Zend REST Server to talk in JSON" I outlined a possible solution for providing URL-addressable resource/model data in JSON format through a RESTful web service. In this post I'd like to show how to unpack the JSON response and render a visual representation by utilizing the Template class of the Prototype JavaScript library.

Requesting the RESTful recordshelf data
As the assumed URL-addressable recordshelf resource is identified either by its unique name or id, it's possible to retrieve the data of a specific shelf via the URL shemes stated in the following table.

IdentifierURL shemeResponse contentHTTP method
namehttp://domain.org/api/recordshelf/shelf/<unique-name>Recordshelf descriptionGET
idhttp://domain.org/api/recordshelf/shelf/<unique-id>""

The public API will respond with a JSON structure describing the requested recordshelf, containing the shelf description and each assigned vinyl item. In the following code listing a part of an initial Behaviour rules definition is stated where specific application parts are glued to the REST API requests, which are performed through the AJAX capabilities of Prototype.

...

'img.getShelfByIdentifier' : function(element){

element.onclick = function(){

// Get the shelf identifier -> <img src="some.img" class="getShelfByIdentifier" id="11"/>
var shelfIdentifier= element.getAttribute('id');

var publicApiRequest = new Ajax.Request('/api/recordshelf/shelf/' + shelfIdentifier, {
method: 'get',
asynchronous: true,
requestHeaders: {Accept: 'text/x-json'},

onSuccess: function(transport, json) {

// JSON object representing a specific shelf
var json = transport.responseText.evalJSON(true);

// Use a custom JavaScript class for rendering
var wrapper = new RecordshelfWrapper(null);
wrapper.render(json, 'shelfDescription');

},
onLoading: function(transport) {

// Display loading spinner and notice

},
onFailure: function(transport) {

// Handle faulty XHR

}
});

}

}
The above approach glues the API request to an image and its assigned onclick event, so it's dependent on an user executed action. Another more suitable and even automated approach would be to spark off the REST API request by using Prototypes built-in Event management. This can be used every time the view, responsible for providing the visual representation, is browsed to. The next code snippet makes a initial request and continues to poll for possible changes on the targeted resource in a 5 minute interval.
Event.observe(window, 'load', function(event) {

var pollRecordshelfChanges = function() {

var resourceIdentifier = 'Classic Material';
var resourceEndpoint = 'http://recordshelf.org/api/recordshelf/shelf/';
var renderTargetElementId = 'shelfRendering';

var wrapper = new RecordshelfWrapper(resourceEndpoint);
wrapper.sparkOffRequestAndRenderResponse(resourceIdentifier, renderTargetElementId);

}
var initialRecordshelfRequest = pollRecordshelfChanges;

// Make the initial request and render the visual representation
initialRecordshelfRequest();

// Make a followup request and render a new representation every 5 minutes
new PeriodicalExecuter(pollRecordshelfChanges, 60 * 5);

});

Unpacking the JSON object
After a succesfull request and the 'parsing' of the response via Prototypes evalJSON() the recordshelf data is available as an JSON object. In the last code listings you might have notice a custom JavaScript class called RecordshelfWrapper, this is where amongst other responsibilities the code for unpacking the properties of the JSON object and the rendering of a visible representation is placed. The following stated render method of this class uses Prototypes Template class to interpolated the JSON object properties into a predefined template.

var RecordshelfWrapper = Class.create();

RecordshelfWrapper.prototype = {

initialize: function(endpoint) {
this.resourceEndpoint = endpoint;
},

sparkOffRequestAndRenderShelf: function(shelfIdentfier, elementId) {
var publicApiRequest = new Ajax.Request(this.resourceEndpoint + shelfIdentfier, {
method: 'get',
asynchronous: true,
requestHeaders: {Accept: 'text/x-json'},

onSuccess: function(transport, json) {

// JSON object representing a specific shelf
var json = transport.responseText.evalJSON(true);
var wrapper = new RecordshelfWrapper(null);
wrapper.render(json, elementId);

},
onLoading: function(transport) {

// Display loading spinner and notice

},
onFailure: function(transport) {

// Handle faulty XHR

}
});
},

render: function(json, elementId) {

var restServerResponseStatus = json.Recordshelf_Service_Provider.getShelfByShelf.status;

if(restServerResponseStatus === '0') {

var restServerResponseMessage = json.Recordshelf_Service_Provider.getShelfByShelf.msg;

var infoNotice = '<div class="infoWrapper">';
infoNotice+= '<img id="info" src="/images/info.png" title="Info message"/>';
infoNotice+= '<div class="infoNote"> #{message}</div></div>';

var infoNoticeTemplate = new Template(infoNotice);
var infoNotice = infoNoticeTemplate.evaluate({message: restServerResponseMessage});

$(elementId).update(infoNotice);
return;
}

var descriptionEntryLevel = json.Recordshelf_Service_Provider.getShelfByShelf.recordshelf;

// Define template layout and keys for recordshelf description
var recordshelfDescription = '<div id="shelfDescription-#{id}" class="shelfDescription">';
recordshelfDescription+= 'Recordshelf name: #{name}<br />';
recordshelfDescription+= 'Recordshelf id: #{id}<br />';
recordshelfDescription+= 'Recordshelf description: #{description}<br />';
recordshelfDescription+= 'Recordshelf genre: #{genre}<br />';
recordshelfDescription+= 'Recordshelf items: #{items}<br />';
recordshelfDescription+= 'Recordshelf owner: #{owner}<br /></div><br />';

var recordshelfDescriptionTemplate = new Template(recordshelfDescription);

// Define mappings of template keys to actual JSON properties
var jsonPropertiesToTemplateKeys = {name: descriptionEntryLevel.name,
id: descriptionEntryLevel.id,
description: descriptionEntryLevel.description,
genre: descriptionEntryLevel.genre,
items: descriptionEntryLevel.items,
owner: descriptionEntryLevel.owner};

// Insert into <div id="elementId"></div>
$(elementId).update(recordshelfDescriptionTemplate.evaluate(jsonPropertiesToTemplateKeys));

var itemsEntryLevel = json.Recordshelf_Service_Provider.getShelfByShelf.records;

// Object to hash
var items = Object.values(itemsEntryLevel);

// Define template layout and keys for vinyl item description
var itemDescription = '<div id="itemDescription-#{id}" class="itemDescription">';
itemDescription+= 'Artistname: #{artistname}<br />';
itemDescription+= 'Relase: #{releasename}(#{releaseyear})<br />';
itemDescription+= 'Recordtype: #{recordtype}<br />';
itemDescription+= 'Label: #{label}<br />';
itemDescription+= 'Genre: #{genre}</div><br />';

var itemDescriptionTemplate = new Template(itemDescription);

items.each(function(item, index) {

// Define mappings of template keys to actual JSON properties for each found vinyl item
jsonPropertiesToTemplateKeys = {id: item.id,
artistname: item.artistname,
releasename: item.releasename,
releaseyear: item.releaseyear,
label: item.label,
genre: item.genre,
recordtype: item.recordtype};

// Append to content already in <div id="elementId"></div>
new Insertion.Bottom($(elementId), itemDescriptionTemplate.evaluate(jsonPropertiesToTemplateKeys));

});

},

...

};

The terminatory image shows a basic visual representation of a requested recordshelf resource in HTML. The general shelf description is contained in the green element at the top and the vinyl items are the ones in the ocher elements.

Rendered JSON response

2 comments:

Benyamin Shoham said...

Nice Article, Are you really building a recordstore or is it just for the sample?

Raphael Stolt said...

Thanks for the props and I just use the domain recordstore for more lively example purposes.