Saturday, December 6, 2008

Instantiating components on template markup

All client-side template engines enable you to create HTML (feel free to go ‘duh’). What they don’t all allow is the creation of event handlers and components over the markup they generate. The general approach with those engines is to do a second pass of code over the markup to create handlers and components. This is quite unfortunate as this generally requires some knowledge of the markup (which plays against separation of concerns) or something like the introduction of marker CSS classes into the markup.

For our own template engine, we wanted event handler creation and component instantiation to be first class scenarios, and we wanted both to be possible from imperative code as well as declarative code.

Imperative code blocks

Let’s start with the imperative approach. The template engine enables the introduction of code blocks right inside the template and also exposes a $element that is a reference to the last created element from the point in the template where it is referenced. So a first approach might be to just hook events and create components from there. Sure enough, that works:

<img sys:id="{{ $id('photo') }}"      sys:src="{{ 'Images/' + Photo }}"      alt="{binding FirstName}" />  <br /> <input type="text" id="{{ $id('firstName') }}"        class="editInPlace name"        value="{binding FirstName}" />  

Please note that the comment-based code block syntax is on its way out and will be replaced in the next preview with an attribute-based alternative. I will give more details when that is available. Suffice it to say for now that this mixing of component instantiation code into markup is not what we intended code blocks for. Code blocks are there to make simple scenarios like conditional rendering and looping over markup as easy as possible.

Unobtrusive and imperative

The better imperative approach looks very much like how you would do things outside of a template if you’re into unobtrusive JavaScript. The way you add event handlers and components over the page’s markup is by subscribing to some page ready event (pageLoad or Sys.Application.add_init in Microsoft Ajax, $(document).ready in jQuery). From that handler, you query the DOM for specific elements and add event handlers and components on those.

To do the same over a template, you handle the itemCreated event of DataView, query the DOM that was just created for the data item and then add event handlers and components.

One problem with repeating markup is to create unique IDs for the generated elements. This is relevant to the problem at hand because referencing elements by ID is by far the most common way. Wouldn’t it be nice to be able to just use getElementByID? Well, in client templates, we provide you with an easy way to both generate unique IDs and to reference elements by ID.

Unique IDs can be generated by the $id function that is part of the execution environment of templates (along with $dataItem, $element, etc.). $id takes a string parameter, which is an ID that is unique within the template, and combines it with the current data item’s index to generate an ID that can be unique within the page:

<img sys:id="{{ $id('photo') }}" sys:src="{{ 'Images/' + Photo }}"      alt="{binding FirstName}" />

To reference those elements -even if you don’t know the pattern $id uses to globalize the id-, you can use the getElementById method that is provided by the template context, which is conveniently available from the event arguments of itemCreated:

args.get_templateContext().getElementById("photo")

Here’s what the code to add an event handler and a behavior looks like:

function onItemCreated(sender, args) {     var context = args.get_templateContext(),         dataItem = args.get_dataItem();     $addHandler(context.getElementById("photo"), "click", function() {         alert("You clicked " + dataItem.FirstName + "'s photo.");     });     $create(Bleroy.Sample.EditInPlace,            { cssClass: "editing" }, {}, {},            context.getElementById("firstName")); }

 

Note: there is a known bug in Preview 3 that prevents getElementByID from working correctly outside of IE. We fixed that bug already for the next preview.

Fully declarative

Of course, if you prefer a fully declarative approach, we allow that too. The template engine understands DOM-0 event handlers in pretty much the same way that the browser does outside templates (we tried to apply the principle of least surprise here). This means that if you specify for example an onclick attribute on an element, it is understood as a string that is the source code for the body of a function that will act as a handler for the click event. The template engine also supports binding expressions on attributes and this is no exception. That means that you can actually build that string expression that will become the body of the handler dynamically using the current data item:

<img sys:id="{{ $id('photo') }}" sys:src="{{ 'Images/' + Photo }}"   alt="{binding FirstName}"   onclick="{{'alert(\'You clicked '+FirstName+'\\\'s photo.\');'}}"/>

Important note: you should be super-careful about building such handler strings on the fly with embedded data: there is potential for injection here, if the FirstName data came from the user or an uncontrolled source. In a real application, you'd want to encode FirstName to escape any quotes. You may useSys.Serialization.JavaScriptSerializer.serialize(FirstName) for example. 

Then, to instantiate the components, you can use our declarative syntax (which will also be the subject of a future post):

<input type="text" id="{{ $id('firstName') }}" class="editInPlace name"        value="{binding FirstName}"        sys:attach="inplace" inplace:cssclass="editing"/>

Conclusion

There are plenty of options to add event handlers and components over template-generated markup in Microsoft Ajax, catering to different coding styles, but we hope we succeeded in keeping the system as simple as possible while keeping all relevant scenarios possible.

Download the full code fro the demo here: 
http://weblogs.asp.net/blogs/bleroy/Samples/EventsAndBehaviorsInTemplates.zip

No comments: