Tuesday, March 15, 2011

Templating

User interfaces are made out of static content, interactive components and dynamic data display as HTML. All those elements are tied together in an HTML document (or a fragment). And we use templates to define those HTML fragments. Templates are stored in the ontology and associated with ontology individuals or classes. In addition template associations are contextualized depending on the kind of display needed (e.g. standalone, as part of a list, full/short etc.). Thus when performing a look for a template to display a particular object, additional contextual parameters determine the actual template to be used. For instance a template displaying an object as standalone, using the whole space available on a page, will likely display all of its properties, while a display as an element of a list will use only most relevant/distinguising properties. 

We are using an officially supported jQuery plugin:

http://api.jquery.com/jquery.tmpl/

It has some limitations. Like nearly all JavaScript templating engines, it starts with some sort of philosophy of how things should be done. But it has enough flexibility by allowing arbitrary function calls and logic inside templates, while dealing with simple cases the same way others do.

Because often the data object needed for a template is an aggregate of several different pieces, sometimes metadata, sometime operational data, often a combination of both, we needed an easy mechanism to assemble it and only then call the template. The main problem is that the template engine can't deal with data obtained asynchronously - it would basically need to delay evaluation and the call to apply the template will need to be asynchronous itself. But there's no support in the jquery.tmpl API for that. So we implemented such a mechanism of delayed evaluation ourselves. One can construct a JavaScript object where each value is actually an instance of AsyncCall:

var obj = { x : new AsyncCall({url:"server:port/etc", async:true, etc...}, function(value) { alert('got x!'); return value; }}

An AsyncCall instance encapsulates the AJAX parameters (mandatory) to use with $.ajax and a callback function (optional) invoked when that particular object property was received at the client. Note that the callback function actually receives the value returned by the server as a parameter and it is expected to return the final value assigned to 'x'. That is, the callback function can "massage" that returned value or return something else completely. 

Given a JavaScript object with AsyncCall values, one can synchronize on the event that they all have been obtained like this:

onObjectReady(obj, function (obj) { alert('all values of ' + obj + ' were received'); };

Below is a complete example of a preliminary version of ServiceDirect-like re-implementation with this framework. The metaService.delay(...) is just a convenience method to construct an AsyncCall for a particular REST service.


load : function (app, parent) {
this.parentElement = parent;
var self = this;
var components = {
InquiryTypesComponent: undefined,
InquiryTableComponent: undefined,
InquiryTable: metaService.delay("/individuals/InquiryTable", function (obj) {
this.InquiryTableComponent = uiEngine.getRenderer(obj.type)(obj);
return "<div id='InquiryTableTopDiv'>";
}),
InquiryTypes: metaService.delay("/classes/sub?parentClass=Inquiry", function (obj) {
self.inquiries = obj.classes;
this.InquiryTypesComponent = self.makeInquiriesDropDown();
return "<div id='InquiryTypesTopDiv'>";
})
};
onObjectReady(components, function() {
if (app.hasTemplate) {
var div = document.createElement("div");
$.tmpl(app.hasTemplate.hasContents, components).appendTo(div);
self.setui(div);
$("#InquiryTypesTopDiv").append(components.InquiryTypesComponent);
$("#InquiryTableTopDiv").append(components.InquiryTableComponent);
}
else
throw new Error('Missing application top level template');
});
}

And here is a portion of the template that is being displayed:

<table width="100%" height="100%" border="0" cellspacing="0" cellpadding="0">
<tbody><tr height="100%">
<td class="contentGrey" dir="ltr">
<table>
<tbody><tr><td>
<table border="0"><tbody><tr><td><span style="width: 1000px">To request a service or report a problem start by selecting the desired service type from the pull-down list. </span></td></tr>
<tr><td><span style="width: 1000px">NOTE: If you do not see the service you would like to request included in the pull-down
list, please contact us by dialing 3-1-1. Call Specialists will be glad to assist you.
If you live outside of Miami-Dade County, please call 305-468-5900.</span></td></tr>
</tbody></table>
{{html InquiryTypes}}
</td></tr>
<tr><td>
</td></tr>
</tbody></table>
</td>
</tr>
</tbody></table>
{{html InquiryTable}}



Note that we use the {{html }} templating tag so the template engine doesn't escape HTML tags from the insert text. Note also that the two object properties used in the template, InquiryTypes and InquiryTable both have a callback associated to convert the data received from the server into HTML text. The callbacks work in tandem with the main (top-level) callback: they construct DOM trees for UI to be inserted and just return 'div' placeholders where those DOM trees are inserted by the top-level callback. This avoids serializing a DOM tree into HTML text and then parsing it back. In the case of InquiryTypes, the data is a list of service/inquiry types and we dynamically create a dropdown box from it. In the case of InquiryTable, the data is an actual UI component described in the ontology and rendered by the client-side UIEngine.

Some more work is perhaps needed to abstract further and simplify this example. Clearly many templates won't need such acrobatics. For instance, templates that simply display data returned by a database query would be rendered in a much simpler way. But here we have a full dynamic interface with complete UI components streamed as metadata from the ontology. So some extra gluing work at the client-side is necessary.

No comments:

Post a Comment