Table of contents
Introduction
As I said in this article, I recently used JST. After playing around with it for 2 or 3 days, I came up with the conclusion that an abstraction layer is needed, except if you want to rewrite the whole code every time !
Creating helpers
JST documentation about the templateObject.process function says :
Note that the '''contextObject''' can contain any JavaScript object, including strings, numbers, date, objects and functions. So, calling ${groupCalender(new Date())} would call contextObject.groupCalender(new Date()). Of course, you would have to supply the groupCalender() function, which should return a string value.
So, we've got a way to define our helpers !
Let's try it :
// As usual our namespace var App = App || {}; /** * a little binding utility relying on closure. * * This function can be improved by handling the * merge of given parameters on binding and given * parameters on call so that : * <pre> * var myBindedFunc = App.bind( myFunc, myContext, param1, param2 ); * myBindedFunc ( param3 ); * <pre> * results being the same as : * <pre> * var myBindedFunc = App.bind( myFunc, myContext ); * myBindedFunc ( param1, param2, param3 ); * <pre> * or, using the call utility : * <pre> * myFunc.call( myContext, param1, param2, param3 ); * <pre> * * @param {Function} afCallback function to apply * @param {Object} aoContext the context to apply the function in * * @return {Function} */ App.bind = function( afCallback, aoContext ){ /* make a copy */ var _fCallback = afCallback; var _oContext = aoContext; /* create the closure */ return function(){ return _fCallback.apply(_oContext, arguments); }; } //let's define our helpers App.Helpers = { /** * used as an helper, writes 'foo' */ test:function(){ return 'foo'; }, /** * increments a value, to demonstrate * the binding necessity for helpers * that needs to access main context */ count:App.bind(function(){ App._counter = App._counter || 0; return App._counter++; },window) }; /* parse and process */ function parse_and_process(){ var dest = document.getElementById('jst_add_helpers_template_test_1_dest'); var tpl = document.getElementById('jst_add_helpers_template_test_1_template'); dest.innerHTML = TrimPath.parseTemplate( tpl.innerHTML).process( App.Helpers ); }
Ready to test ?
Give it a try !
Here will come the processed template
So, It seems that works, but why the hell did I use a binding ?
Well, it's all about contexts, the template is executed in process function's first parameter context. That means you are enclosed in this context, so you can't access the other variables ( such as App in the count helper's case ).
So, whenever we need to create an helper that has to access other namespaces, it is necessary to bind it to needed context. Application field of such a technique can be:
- Internationalization (to access datas that are not presents)
- add custom events
- register or modify variables
- ...
Well, That's cool, we've done with helpers definition understanding, but it can be exhausting to select helpers every-time you call a template, let's see how to make it simple.
Helpers and templating automation
Declare the namespaces
As usual, automation means a new layer. We will need as usual a main namespace, and following sub-namespace.
- App
- What a surprise ! Just the same as usual :-). Well, the main namespace
- App.Templates
- The template engine wrapper namespace
- App.Templates.Helpers
- the templates helpers namespace
- Our templateObject overlay
Parser wrapper : App.Templates
The parser wrapper needs first to keep a trace of our parsed templates, so let's have a storage facility :
/* namespace */ var App; App.Templates = App.Templates || {}; /* utilities */ /** * copy all the properties from aoSource to aoDestination * @param {Object} aoDestination * @param {Object} aoSource * @return {Object} */ App.extend = function(aoDestination, aoSource){ for (var property in aoSource) aoDestination[property] = aoSource[property]; return aoDestination; }; /* storage utilities */ App.Templates.stored = {}; /** * stores a template * * @param {String} asTemplate the template name * @param {OGF.Templates.templateObject} asTemplate the parsed template * * @return {OGF.Templates.templateObject} */ App.Templates.store = function( asTemplateName, aoTemplate ){ return App.Templates.stored[asTemplateName] = aoTemplate; }; /** * reads a template * * @param {String} asTemplate the template name * * @return {OGF.Templates.templateObject} */ App.Templates.find = function( asTemplateName ){ return App.Templates.stored[asTemplateName]; };
Easy isn't it ? Now the parsing layer :
/* parser */ /** * parses a template * this function is a wrapper for the template. * * @param {String} asTemplate the template string to parse * * @return {OGF.Templates.templateObject} */ App.Templates.parse = function( asTemplate ){ return new App.Templates.templateObject( TrimPath.parseTemplate(asTemplate) ); }; /** * parses a template and store it. * This function main goal is to be binded on the request return. * * @param {String} asTemplate the template string to parse * * @return {OGF.Templates.templateObject} */ App.Templates.parseAndStore = function( asName, asTemplate ){ App.Template.store( asName, App.Templates.parse( asTemplate ) ); };
Well, you maybe noticed that we used an undeclared class called App.Templates.templateObject. Let's declare it.
templateObject overlay : App.Templates.templateObject
This class is our abstraction layer over the TrimPath template object.
It just adds our helpers definition to the evaluation context.
/** * @constructor * @classDescription the template object wrapper for OGF * @param {templateObject} aoTemplate the template object */ App.Templates.templateObject = function(aoTemplate){ this.initialize(aoTemplate); } App.Templates.templateObject.prototype = { /** * the template object reference * @var {templateObject} */ _template:null, /** * initializes the object * @param {templateObject} aoTemplate the template object */ initialize:function(aoTemplate){ this._template = aoTemplate; }, process:function( aoDatas ){ var _oDatas = {}; if(App.Templates.Helpers) { _oDatas = App.extend( _oDatas, App.Templates.Helpers); } _oDatas = App.extend( _oDatas, aoDatas || {}); return this._template.process(_oDatas); } };
Helpers definition
Well, to define helpers you just need to add them to the App.Templates.Helpers namespace. Here are some of mine :
App.Templates.Helpers = { /** * include an already compiled template * * @param {Object} asTplName * @param {Object} context */ include:(function( asTplName, aoContext){ return App.Templates.find(asTplName).process(aoContext); }).bind(window), /** * write a tag opening with the given tagname and the given properties * example : * ${%open_tag('div', {id:"foo",class:"bar",style:{width:'50%',background:'#000',color:'#fff'}})%} * I'm currently looking for a solution to make it more readable */ open_tag:function( asTagName, aoProperties ){ var _oProperties = aoProperties || {}; var _aProperties = [asTagName]; for( var _i in _oProperties) { var _val = ''; if(typeof(_oProperties[_i]) == 'object') { _val = []; for(var _j in _oProperties[_i] ) { _val.push(_j+':'+_oProperties[_i][_j]); } _val = _val.join(';'); } else { _val = _oProperties[_i]; } _aProperties.push(_i+'="'+_val+'"'); } return '<'+_aProperties.join(" ")+'>'; } }
To conclude, I'll say that I use this for the binding when I include libraries into a closure (As I do for every code in this blog) to avoid polluting the main namespace with utilities functions or prototype extensions (just as used in the JST code with the array prototype for IE5 bugfix)
Conclusion
Some improvements can be done, such as utilities functions to register new helpers or a function to update the element with template result (and optionally evaluate scripts).
You can also allow some options to determines which modifiers to use, to auto-load them...