Introducing MooTools Templated
One major problem with creating UI components with the MooTools JavaScript framework is that there isn't a great way of allowing customization of template and ease of node creation. As of today, there are two ways of creating:
new Element Madness
The first way to create UI-driven widgets with MooTools is creating numerous elements programmatically. The following snippet is taken from my MooTools LightFace plugin:
//draw rows and cells; use native JS to avoid IE7 and I6 offsetWidth and offsetHeight issues var verts = ['top','center','bottom'], hors = ['Left','Center','Right'], len = verts.length; for(var x = 0; x < len; x++) { var row = this.box.insertRow(x); for(var y = 0; y < len; y++) { var cssClass = verts[x] + hors[y], cell = row.insertCell(y); cell.className = cssClass; if (cssClass == 'centerCenter') { this.contentBox = new Element('div',{ 'class': 'lightfaceContent', styles: { width: this.options.width } }); cell.appendChild(this.contentBox); } else { document.id(cell).setStyle('opacity',0.4); } } } //draw title this.title = new Element('h2',{ 'class': 'lightfaceTitle', html: this.options.title }).inject(this.contentBox); //draw message box this.messageBox = new Element('div',{ 'class': 'lightfaceMessageBox', html: this.options.content || '', styles: { height: this.options.height } }).inject(this.contentBox); //button container this.footer = new Element('div',{ 'class': 'lightfaceFooter', styles: { display: 'none' } }).inject(this.contentBox); //draw overlay this.overlay = new Element('div',{ html: ' ', styles: { opacity: 0 }, 'class': 'lightfaceOverlay', tween: { link: 'chain', duration: this.options.fadeDuration, onComplete: function() { if(this.overlay.getStyle('opacity') == 0) this.box.focus(); }.bind(this) } }).inject(this.contentBox);
The problem with creating elements programmatically within widgets is that doing so means your class becomes inflexible. What if you want one of the elements to have a specific CSS class? What if you want one of the elements to be a DIV instead of a SPAN? What if you don't want one or more of the elements generated by the class? You would need to extend the class and override the method that created all of the elements. Yuck.
User Reliance
The second way to create UI-driven classes is to rely on the developers using your classes to provide the correct elements to the class in the correct hierarchy. This could include providing elements to the initialize method of a class' instance. I dislike this method for complex widgets because there's too much reliance on the user to figure out what elements your class needs.
The Solution: MooTools Templated
Templated is a MooTools mixin class which creates elements for classes using a string-based HTML template which may included embedded attach points and events. Templated is very much inspired by the Dojo Toolkit's dijit._Template
resource, which has been tried, tested, and proven in Dojo's outstanding Dijit UI framework. Let's explore what Templated is and how to use it!
Templates and Terms
Before using the Templated mixin, it's important to understand a few concepts and terms. Take the following template for example:
<div class='UIWidget'> <p>{message}</p> <input type='text' data-widget-attach-point='inputNode' data-widget-attach-event='keyup:onKeyUp,focus:onFocus' /> <div type='submit' data-widget-type='UIWidgetButton' data-widget-attach-point='buttonWidget,submitWidget' data-widget-attach-event='click:onSubmit' data-widget-props='label:\"My Prop Label\",somethingElse:true'></div> </div>
Within the template you'll see attach points, attach events, and props. Attach points are named properties which will be attached to the widget instance and map to the given node within the template. Attach events are key (event type) => value (event handler) mappings separated by a colon. Multiple attach points can refer to the same element, and an elements may have many attach events; both a comma-separated. Props are configuration properties for the node's given instance if the node is mean to be a widget itself. Props are written in JSON-style syntax and property keys should match the element options; the options and props (if they exist) are merged.
The following snippet should provide a bit more clarity as to what each mean:
// Representation of an instance created with Templated { buttonWidget: "<input type='submit' />", inputNode: "<input type='text' />", onKeyUp: function() { }, // fires upon keyup on the text field onFocus: function() { }, // fires upon focus event on the text field onSubmit: function() { }, // fires upon submit button click submitWidget: "<input type='submit' />" // More classes here }
Attach points, events, and props are all very simply to use but provide an essential function for maximum customization in UI widget templating. Templated also looks for string substitution (String.substitute
) opportunities, allowing you to add basic variables within your templates.
Using MooTools Templated
To use Templated within MooTools, add Templated to your class' Implements
array:
Implements: [Options, Templated]
With Templated available within the class, a few more options are available within the class:
- template: The string HTML template for the class, including attach points, events, props, and subwidgets.
- templateUrl: The URL to the widget's template if it's external. A basic Request will be sent to retrieve the widget if not yet cached.
- element: The element which the widget's domNode will replace.
- widgetsInTemplate: If true, parses the widget template to find and create subwidgets. Inline attach points and attach events are added to the parent widget, not the subwidget.
- propertyMappings: An object which contains custom property mappings for the widget.
- defaultPropertyMappings: An object with default, fallback property mappings for the widget. Property mappings included ID, style, and class, which these properties are carried over from the base element to the domNode.
When you desire for the template to be parsed and nodes to be created, calling this.parse()
will accomplish that task. You will usually want to call this method within the initialize method after the options have been set:
// The magical constructor initialize: function(options) { // Set the options this.setOptions(options); // Parse the template this.parse(); },
With the attach points and events in place, you can code your UI class per usual, using the attach points to refer to nodes when needed. Here's a sample UI widget with subwidgets:
var UIWidget = new Class({ // Implement the new Templated class Implements: [Options, Templated], // The UI template with attachpoints and attachevents options: { template: "" + "", uiwidgetOption: "here!" }, // The magical constructor initialize: function(options) { this.debug("[widget] Initialize"); // Set the options this.setOptions(options); // Parse the template this.parse(); }, onKeyUp: function(evt) { }, onFocus: function() { this.inputNode.set("value", ""); }, onSubmit: function(evt) { evt.stop(); }, onMouseEnter: function() { this.domNode.setStyle("background","pink"); }, onMouseLeave: function() { this.domNode.setStyle("background","lightblue"); } }); // Create a button widget var UIWidgetButton = new Class({ // Implement the new Templated class Implements: [Options, Templated], // The UI template with attachpoints and attachevents options: { template: "", uiwidgetOption: "here too!", label: "Default Submit Option" }, // The magical constructor initialize: function(options) { // Set the options this.setOptions(options); // Parse the template this.parse(); }, // onClick onClick: function(evt) { evt.stop(); } });{message}
" + "" + "" + "
Your UI widget has been created with flexibility and ease of use in mind!
Templated Events
Templated provides stub methods along the way so code can be executed at different points within the creation of the widget:
- postMixInProperties: Fires after the options and widget properties have been mixed.
- postCreate: Fires after the widget nodes have been created but before the nodes are placed into the DOM
- startup: Fires after the widget has been created and is placed into the DOM
Templated Helpers
Templated also provides two essential widget helpers: Element.parse and document.widget. Element.parse allows for declarative (node-based) widget creation. So if your page contains nodes with data-widget-type
properties, you can use the parse method of elements to find widgets and subwidgets. Take the following HTML:
<!-- Declarative --> <div id="qHolder2" data-widget-type="UIWidget" data-widget-props="message:'This is the second widget!'" class="UIWidgetInstance2"></div>
Running the following JavaScript snippet would parse the page, find the DIV
, and create a widget from it!
document.body.parse();
The document.widget method accepts a DOM node and returns the widget object which it represents:
var myWidget = document.widget("qHolder2");
Being able to retrieve a widget based on a node is very helpful in debugging your application.
Realistic Usage
One of the popular UI plugins I've created is LightFace, the Facebook-like lightbox. Unfortunately LightFace falls victim to the "new Element Madness" because of the complexity of the widget structure. Many people were unhappy about its table-based structure (which accommodated for IE6) and wanted a DIV-based structure...and with Templated, you can make that happen.
Here's what LightFace would look like with Templated:
<table class="lightface" data-widget-attach-point="box"> <tbody> <tr> <td class="topLeft"></td> <td class="topCenter"></td> <td class="topRight"></td> </tr> <tr> <td class="centerLeft"></td> <td class="centerCenter"> <div class="lightfaceContent" data-widget-attach-point="contentBox"> <h2 class="lightfaceTitle lightfaceDraggable" data-widget-attach-point="title">{title}</h2> <div class="lightfaceMessageBox" data-widget-attach-point="messageBox">{message}</div> <div class="lightfaceFooter" data-widget-attach-point="footer"></div> <div class="lightfaceOverlay" data-widget-attach-point="overlay"></div> </div> </td> <td class="centerRight"></td> </tr> <tr> <td class="bottomLeft"></td> <td class="bottomCenter"></td> <td class="bottomRight"></td> </tr> </tbody> </table>
And if you restructured the LightFace CSS file to accommodate for DIV-based structure, the template could look like:
<div class="lightface" data-widget-attach-point="box"> <div class="lightfaceContent" data-widget-attach-point="contentBox"> <h2 class="lightfaceTitle lightfaceDraggable" data-widget-attach-point="title">{title}</h2> <div class="lightfaceMessageBox" data-widget-attach-point="messageBox">{message}</div> <div class="lightfaceFooter" data-widget-attach-point="footer"></div> <div class="lightfaceOverlay" data-widget-attach-point="overlay"></div> </div> </div>
The presentation state of widget would be completely in the developer's hands with Templated!
BETA!
Templated is currently in BETA state, so please report any bugs you find along the way. A stable UI solution for MooTools will do everyone a favor.
Get Templated!
The ability to apply this level of control to widget templating is amazing. Dijit's use of _Templated help makes the UI framework the best available for any JavaScript framework. Please consider using Templated for your UI work, especially if your components are open source or maintained by a team. Have fun with your UIs!
Brilliant! I can’t wait to use this. I was going to write something similar (but much less robust) but I never had the need/chance to. And now I don’t have to!
Be sure to send me suggestions and bug reports when you start using it heavily.
Oh well, it’s a very useful thing :)
I’d like to have a HUGE repository with base and custom widget….
a community widget repo!
Definitely a needed addition. Thanks David, will be checking it out!
Hey David,
Great article and something I really want to get my head around at the moment I am so far away from understanding whats going on its scary!
Why in your demo does the
UIWidgetButton
not take on the default label"Default Submit Option"
?I changed the
UIWidget
template (button part) todata-widget-props='label:\"{label}\"
and it worked for the first sample but not when we load the external template. Do you know why?
Please ignore previous question, the answer was pretty simple, add
data-widget-props="label:'{label}'"
to the node callingUIWidget
(in the external html file) and in turn it will be available when callingUIWidgetButton
in theUIWidget
templateThanks
David, could you please tel me why Templated doesn’t work as ecpected with mooTools 1.4,
It’s not keeping class names specified in the template string.
Hi David,
your template engine sounds great! Which version of mootools is required? Is it already “stable” / developed actively?
Hey Georg!
The current version of Moo is recommended, but I haven’t had a chance to continue maintaining Templated. I welcome any feedback though!
hi david,
As always, you help me this time also. I am using lazy load plugin in drupal 6 website. and having this problem with all new browsers. I got idea by reading your code. Thanks a lot !!!
Thank you,
Kaustubh