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}" />
<!--*
$addHandler($element, "click", function() {
    alert("You clicked " + FirstName + "'s photo.");
});
*-->
<br />
<input type="text" id="{{ $id('firstName') }}"
       class="editInPlace name"
       value="{binding FirstName}" />
<!--*
$create(Bleroy.Sample.EditInPlace,
    { cssClass: "editing" }, {}, {}, $element);
*-->

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 use Sys.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"/>

What it looks like

Here’s what the page looks like (no matter which of the three versions of the page you choose):

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

6 Comments

  • Interesting. But I'm not seeing the 'unobtrusive' part.

    ie. jQuery is 'unobtrusive'.

    I think unobtrusive would be completely separated from the html.

    Unobtrusive to me would be that I could have 3 html pages of the exact same content with 3 different css styles or js files that 'act upon' the html. The html would be untouched by it.

    To be honest, I'd rather see MS go a different route here: you have WPF for windows apps - xaml. Silverlight with xaml. How about creating xaml on the web as well ?

    Basically a xaml based view engine.

  • Bertrand - Do you have any samples that would use the MVC project as a starting point? Data sources from controller methods as well as usage of the template engine in a project that has master pages (where the is not present in each .aspx page.

    Thanks, Dennis

  • @Steve: so if I'm following you, you think unobtrusive means that even the data should be introduced in the template by script? You sure can do that but then the advantages of using templates become pretty small then. But if that's your thing, sure, go ahead and do that. We do support jQuery after all...
    We had something closer to XAML in xml-script three years ago, also experimented with XAML for HTML and JS and it really didn't work very well. The problem is that you're not starting from scratch here like WPF was: you have HTML to deal with. We chose instead to use legal extensions to XHTML and we're quite happy with it. Your feedback is actually one of very few stating this is not the right approach out of overwhelmingly positive feedback. Not saying you're wrong, just that this works better for more people.

    @Dennis: this is just HTML, you can apply that to MVC very easily. You can for example use a JSON result as the server data source. For the master page scenario, you can actually do the namespace declaration on the dataview's tag, and it will work just as well as on body. You can also call Sys.Application.processNode or processNodes to avoid having to touch body to activate the DOM.

  • @Edward: nobody claimed the final declarative sample was unobtrusive. Now the markup in the section before that contains only one thing that is not pure HTML: the binding expressions. If you think this is out of place, I guess you would find *any* template engine not pure enough for your taste. Which is fine, you still have plenty of options, but you may be missing on some major productivity gains.
    Now the server-side approach to abstract the client-side stuff is something we've also done (and many, many others too, way before Gaiaware), and it works in many cases but nothing ever beats the pure client approach, if only because you can have a client-side representation of the data.

  • This is a pretty innovative templating engine Bertrand! One quick question:

    What's the binding expression for the current JSON object? For example, in my template I have

    {{Name}}

    Util.getBusinessDetailsUrl is my own function that takes in the entire bound object, not just a field ("Name" is a field of my object for example). Is {{this}} correct?

  • @James: the correct markup for this would probably be {{Name}}
    Notice how the binding expression takes the whole attribute value.

Comments have been disabled for this content.