Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / jsbuilder / src / XTemplate.js
1 /**
2  * @class Ext.XTemplate
3  * @extends Ext.Template
4  * <p>A template class that supports advanced functionality like:<div class="mdetail-params"><ul>
5  * <li>Autofilling arrays using templates and sub-templates</li>
6  * <li>Conditional processing with basic comparison operators</li>
7  * <li>Basic math function support</li>
8  * <li>Execute arbitrary inline code with special built-in template variables</li>
9  * <li>Custom member functions</li>
10  * <li>Many special tags and built-in operators that aren't defined as part of
11  * the API, but are supported in the templates that can be created</li>
12  * </ul></div></p>
13  * <p>XTemplate provides the templating mechanism built into:<div class="mdetail-params"><ul>
14  * <li>{@link Ext.view.View}</li>
15  * </ul></div></p>
16  *
17  * <p>For example usage {@link #XTemplate see the constructor}.</p>
18  *
19  * @constructor
20  * The {@link Ext.Template#Template Ext.Template constructor} describes
21  * the acceptable parameters to pass to the constructor. The following
22  * examples demonstrate all of the supported features.</p>
23  *
24  * <div class="mdetail-params"><ul>
25  *
26  * <li><b><u>Sample Data</u></b>
27  * <div class="sub-desc">
28  * <p>This is the data object used for reference in each code example:</p>
29  * <pre><code>
30 var data = {
31     name: 'Tommy Maintz',
32     title: 'Lead Developer',
33     company: 'Sencha Inc.',
34     email: 'tommy@sencha.com',
35     address: '5 Cups Drive',
36     city: 'Palo Alto',
37     state: 'CA',
38     zip: '44102',
39     drinks: ['Coffee', 'Soda', 'Water'],
40     kids: [{
41         name: 'Joshua',
42         age:3
43     },{
44         name: 'Matthew',
45         age:2
46     },{
47         name: 'Solomon',
48         age:0
49     }]
50 };
51    </code></pre>
52  * </div>
53  * </li>
54  *
55  *
56  * <li><b><u>Auto filling of arrays</u></b>
57  * <div class="sub-desc">
58  * <p>The <b><tt>tpl</tt></b> tag and the <b><tt>for</tt></b> operator are used
59  * to process the provided data object:
60  * <ul>
61  * <li>If the value specified in <tt>for</tt> is an array, it will auto-fill,
62  * repeating the template block inside the <tt>tpl</tt> tag for each item in the
63  * array.</li>
64  * <li>If <tt>for="."</tt> is specified, the data object provided is examined.</li>
65  * <li>While processing an array, the special variable <tt>{#}</tt>
66  * will provide the current array index + 1 (starts at 1, not 0).</li>
67  * </ul>
68  * </p>
69  * <pre><code>
70 &lt;tpl <b>for</b>=".">...&lt;/tpl>       // loop through array at root node
71 &lt;tpl <b>for</b>="foo">...&lt;/tpl>     // loop through array at foo node
72 &lt;tpl <b>for</b>="foo.bar">...&lt;/tpl> // loop through array at foo.bar node
73    </code></pre>
74  * Using the sample data above:
75  * <pre><code>
76 var tpl = new Ext.XTemplate(
77     '&lt;p>Kids: ',
78     '&lt;tpl <b>for</b>=".">',       // process the data.kids node
79         '&lt;p>{#}. {name}&lt;/p>',  // use current array index to autonumber
80     '&lt;/tpl>&lt;/p>'
81 );
82 tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
83    </code></pre>
84  * <p>An example illustrating how the <b><tt>for</tt></b> property can be leveraged
85  * to access specified members of the provided data object to populate the template:</p>
86  * <pre><code>
87 var tpl = new Ext.XTemplate(
88     '&lt;p>Name: {name}&lt;/p>',
89     '&lt;p>Title: {title}&lt;/p>',
90     '&lt;p>Company: {company}&lt;/p>',
91     '&lt;p>Kids: ',
92     '&lt;tpl <b>for="kids"</b>>',     // interrogate the kids property within the data
93         '&lt;p>{name}&lt;/p>',
94     '&lt;/tpl>&lt;/p>'
95 );
96 tpl.overwrite(panel.body, data);  // pass the root node of the data object
97    </code></pre>
98  * <p>Flat arrays that contain values (and not objects) can be auto-rendered
99  * using the special <b><tt>{.}</tt></b> variable inside a loop.  This variable
100  * will represent the value of the array at the current index:</p>
101  * <pre><code>
102 var tpl = new Ext.XTemplate(
103     '&lt;p>{name}\&#39;s favorite beverages:&lt;/p>',
104     '&lt;tpl for="drinks">',
105        '&lt;div> - {.}&lt;/div>',
106     '&lt;/tpl>'
107 );
108 tpl.overwrite(panel.body, data);
109    </code></pre>
110  * <p>When processing a sub-template, for example while looping through a child array,
111  * you can access the parent object's members via the <b><tt>parent</tt></b> object:</p>
112  * <pre><code>
113 var tpl = new Ext.XTemplate(
114     '&lt;p>Name: {name}&lt;/p>',
115     '&lt;p>Kids: ',
116     '&lt;tpl for="kids">',
117         '&lt;tpl if="age &amp;gt; 1">',
118             '&lt;p>{name}&lt;/p>',
119             '&lt;p>Dad: {<b>parent</b>.name}&lt;/p>',
120         '&lt;/tpl>',
121     '&lt;/tpl>&lt;/p>'
122 );
123 tpl.overwrite(panel.body, data);
124    </code></pre>
125  * </div>
126  * </li>
127  *
128  *
129  * <li><b><u>Conditional processing with basic comparison operators</u></b>
130  * <div class="sub-desc">
131  * <p>The <b><tt>tpl</tt></b> tag and the <b><tt>if</tt></b> operator are used
132  * to provide conditional checks for deciding whether or not to render specific
133  * parts of the template. Notes:<div class="sub-desc"><ul>
134  * <li>Double quotes must be encoded if used within the conditional</li>
135  * <li>There is no <tt>else</tt> operator &mdash; if needed, two opposite
136  * <tt>if</tt> statements should be used.</li>
137  * </ul></div>
138  * <pre><code>
139 &lt;tpl if="age &gt; 1 &amp;&amp; age &lt; 10">Child&lt;/tpl>
140 &lt;tpl if="age >= 10 && age < 18">Teenager&lt;/tpl>
141 &lt;tpl <b>if</b>="this.isGirl(name)">...&lt;/tpl>
142 &lt;tpl <b>if</b>="id==\'download\'">...&lt;/tpl>
143 &lt;tpl <b>if</b>="needsIcon">&lt;img src="{icon}" class="{iconCls}"/>&lt;/tpl>
144 // no good:
145 &lt;tpl if="name == "Tommy"">Hello&lt;/tpl>
146 // encode &#34; if it is part of the condition, e.g.
147 &lt;tpl if="name == &#38;quot;Tommy&#38;quot;">Hello&lt;/tpl>
148  * </code></pre>
149  * Using the sample data above:
150  * <pre><code>
151 var tpl = new Ext.XTemplate(
152     '&lt;p>Name: {name}&lt;/p>',
153     '&lt;p>Kids: ',
154     '&lt;tpl for="kids">',
155         '&lt;tpl if="age &amp;gt; 1">',
156             '&lt;p>{name}&lt;/p>',
157         '&lt;/tpl>',
158     '&lt;/tpl>&lt;/p>'
159 );
160 tpl.overwrite(panel.body, data);
161    </code></pre>
162  * </div>
163  * </li>
164  *
165  *
166  * <li><b><u>Basic math support</u></b>
167  * <div class="sub-desc">
168  * <p>The following basic math operators may be applied directly on numeric
169  * data values:</p><pre>
170  * + - * /
171  * </pre>
172  * For example:
173  * <pre><code>
174 var tpl = new Ext.XTemplate(
175     '&lt;p>Name: {name}&lt;/p>',
176     '&lt;p>Kids: ',
177     '&lt;tpl for="kids">',
178         '&lt;tpl if="age &amp;gt; 1">',  // <-- Note that the &gt; is encoded
179             '&lt;p>{#}: {name}&lt;/p>',  // <-- Auto-number each item
180             '&lt;p>In 5 Years: {age+5}&lt;/p>',  // <-- Basic math
181             '&lt;p>Dad: {[parent.name]}&lt;/p>',
182         '&lt;/tpl>',
183     '&lt;/tpl>&lt;/p>'
184 );
185 tpl.overwrite(panel.body, data);
186    </code></pre>
187  * </div>
188  * </li>
189  *
190  *
191  * <li><b><u>Execute arbitrary inline code with special built-in template variables</u></b>
192  * <div class="sub-desc">
193  * <p>Anything between <code>{[ ... ]}</code> is considered code to be executed
194  * in the scope of the template. There are some special variables available in that code:
195  * <ul>
196  * <li><b><tt>values</tt></b>: The values in the current scope. If you are using
197  * scope changing sub-templates, you can change what <tt>values</tt> is.</li>
198  * <li><b><tt>parent</tt></b>: The scope (values) of the ancestor template.</li>
199  * <li><b><tt>xindex</tt></b>: If you are in a looping template, the index of the
200  * loop you are in (1-based).</li>
201  * <li><b><tt>xcount</tt></b>: If you are in a looping template, the total length
202  * of the array you are looping.</li>
203  * </ul>
204  * This example demonstrates basic row striping using an inline code block and the
205  * <tt>xindex</tt> variable:</p>
206  * <pre><code>
207 var tpl = new Ext.XTemplate(
208     '&lt;p>Name: {name}&lt;/p>',
209     '&lt;p>Company: {[values.company.toUpperCase() + ", " + values.title]}&lt;/p>',
210     '&lt;p>Kids: ',
211     '&lt;tpl for="kids">',
212        '&lt;div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
213         '{name}',
214         '&lt;/div>',
215     '&lt;/tpl>&lt;/p>'
216 );
217 tpl.overwrite(panel.body, data);
218    </code></pre>
219  * </div>
220  * </li>
221  *
222  * <li><b><u>Template member functions</u></b>
223  * <div class="sub-desc">
224  * <p>One or more member functions can be specified in a configuration
225  * object passed into the XTemplate constructor for more complex processing:</p>
226  * <pre><code>
227 var tpl = new Ext.XTemplate(
228     '&lt;p>Name: {name}&lt;/p>',
229     '&lt;p>Kids: ',
230     '&lt;tpl for="kids">',
231         '&lt;tpl if="this.isGirl(name)">',
232             '&lt;p>Girl: {name} - {age}&lt;/p>',
233         '&lt;/tpl>',
234         // use opposite if statement to simulate 'else' processing:
235         '&lt;tpl if="this.isGirl(name) == false">',
236             '&lt;p>Boy: {name} - {age}&lt;/p>',
237         '&lt;/tpl>',
238         '&lt;tpl if="this.isBaby(age)">',
239             '&lt;p>{name} is a baby!&lt;/p>',
240         '&lt;/tpl>',
241     '&lt;/tpl>&lt;/p>',
242     {
243         // XTemplate configuration:
244         compiled: true,
245         // member functions:
246         isGirl: function(name){
247             return name == 'Sara Grace';
248         },
249         isBaby: function(age){
250             return age < 1;
251         }
252     }
253 );
254 tpl.overwrite(panel.body, data);
255    </code></pre>
256  * </div>
257  * </li>
258  *
259  * </ul></div>
260  *
261  * @param {Mixed} config
262  */
263 Ext.XTemplate = function() {
264     Ext.XTemplate.superclass.constructor.apply(this, arguments);
265
266     var me = this,
267         s = me.html,
268         re = /<tpl\b[^>]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/,
269         nameRe = /^<tpl\b[^>]*?for="(.*?)"/,
270         ifRe = /^<tpl\b[^>]*?if="(.*?)"/,
271         execRe = /^<tpl\b[^>]*?exec="(.*?)"/,
272         id = 0,
273         tpls = [],
274         VALUES = 'values',
275         PARENT = 'parent',
276         XINDEX = 'xindex',
277         XCOUNT = 'xcount',
278         RETURN = 'return ',
279         WITHVALUES = 'with(values){ ',
280         m,
281         m2,
282         m3,
283         m4,
284         exp,
285         fn,
286         exec,
287         name,
288         i;
289
290     s = ['<tpl>', s, '</tpl>'].join('');
291
292     while ((m = s.match(re))) {
293         m2 = m[0].match(nameRe);
294         m3 = m[0].match(ifRe);
295         m4 = m[0].match(execRe);
296         exp = null;
297         fn = null;
298         exec = null;
299         name = m2 && m2[1] ? m2[1] : '';
300
301         if (m3) {
302             exp = m3 && m3[1] ? m3[1] : null;
303             if (exp) {
304                 fn = new Function(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + 'try{' + RETURN + (Ext.util.Format.htmlDecode(exp)) + ';}catch(e){return;}}');
305             }
306         }
307         if (m4) {
308             exp = m4 && m4[1] ? m4[1] : null;
309             if (exp) {
310                 exec = new Function(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + (Ext.util.Format.htmlDecode(exp)) + '; }');
311             }
312         }
313         if (name) {
314             switch (name) {
315             case '.':
316                 name = new Function(VALUES, PARENT, WITHVALUES + RETURN + VALUES + '; }');
317                 break;
318             case '..':
319                 name = new Function(VALUES, PARENT, WITHVALUES + RETURN + PARENT + '; }');
320                 break;
321             default:
322                 name = new Function(VALUES, PARENT, WITHVALUES + RETURN + name + '; }');
323             }
324         }
325         tpls.push({
326             id: id,
327             target: name,
328             exec: exec,
329             test: fn,
330             body: m[1] || ''
331         });
332         s = s.replace(m[0], '{xtpl' + id + '}');
333         ++id;
334     }
335     for (i = tpls.length - 1; i >= 0; --i) {
336         me.compileTpl(tpls[i]);
337     }
338     me.master = tpls[tpls.length - 1];
339     me.tpls = tpls;
340 };
341 Ext.extend(Ext.XTemplate, Ext.Template, {
342     re: /\{([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?\}/g,
343     
344     /**
345      * @cfg {RegExp} codeRe The regular expression used to match code variables (default: matches <tt>{[expression]}</tt>).
346      */
347     codeRe: /\{\[((?:\\\]|.|\n)*?)\]\}/g,
348
349     // @private
350     applySubTemplate: function(id, values, parent, xindex, xcount) {
351         var me = this,
352             len,
353             t = me.tpls[id],
354             vs,
355             buf = [],
356             i;
357         if ((t.test && !t.test.call(me, values, parent, xindex, xcount)) ||
358         (t.exec && t.exec.call(me, values, parent, xindex, xcount))) {
359             return '';
360         }
361         vs = t.target ? t.target.call(me, values, parent) : values;
362         len = vs.length;
363         parent = t.target ? values: parent;
364         if (t.target && Ext.isArray(vs)) {
365             for (i = 0, len = vs.length; i < len; i++) {
366                 buf[buf.length] = t.compiled.call(me, vs[i], parent, i + 1, len);
367             }
368             return buf.join('');
369         }
370         return t.compiled.call(me, vs, parent, xindex, xcount);
371     },
372
373     // @private
374     compileTpl: function(tpl) {
375         var fm = Ext.util.Format,
376             useF = this.disableFormats !== true,
377             body;
378
379         function fn(m, name, format, args, math) {
380             var v;
381             // name is what is inside the {}
382
383             // Name begins with xtpl, use a Sub Template
384             if (name.substr(0, 4) == 'xtpl') {
385                 return "',this.applySubTemplate(" + name.substr(4) + ", values, parent, xindex, xcount),'";
386             }
387             // name = "." - Just use the values object.
388             if (name == '.') {
389                 v = 'typeof values == "string" ? values : ""';
390             }
391
392             // name = "#" - Use the xindex
393             else if (name == '#') {
394                 v = 'xindex';
395             }
396
397             // name has a . in it - Use object literal notation, starting from values
398             else if (name.indexOf('.') != -1) {
399                 v = "values."+name;
400             }
401
402             // name is a property of values
403             else {
404                 v = "values['" + name + "']";
405             }
406             if (math) {
407                 v = '(' + v + math + ')';
408             }
409             if (format && useF) {
410                 args = args ? ',' + args: "";
411                 if (format.substr(0, 5) != "this.") {
412                     format = "fm." + format + '(';
413                 }
414                 else {
415                     format = 'this.call("' + format.substr(5) + '", ';
416                     args = ", values";
417                 }
418             }
419             else {
420                 args = '';
421                 format = "(" + v + " === undefined ? '' : ";
422             }
423             return "'," + format + v + args + "),'";
424         }
425
426         function codeFn(m, code) {
427             // Single quotes get escaped when the template is compiled, however we want to undo this when running code.
428             return "',(" + code.replace(/\\'/g, "'") + "),'";
429         }
430         body = ["tpl.compiled = function(values, parent, xindex, xcount){return ['"];
431         body.push(tpl.body.replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.re, fn).replace(this.codeRe, codeFn));
432         body.push("'].join('');};");
433         body = body.join('');
434         eval(body);
435         return this;
436     },
437
438     /**
439       * Returns an HTML fragment of this template with the specified values applied.
440       * @param {Object} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'})
441       * @return {String} The HTML fragment
442       */
443     applyTemplate: function(values) {
444         return this.master.compiled.call(this, values, {}, 1, 1);
445     },
446
447     /**
448       * Compile the template to a function for optimized performance.  Recommended if the template will be used frequently.
449       * @return {Function} The compiled function
450       */
451     compile: function() {
452         return this;
453     }
454 });
455 /**
456   * Alias for {@link #applyTemplate}
457   * Returns an HTML fragment of this template with the specified values applied.
458   * @param {Object/Array} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'})
459   * @return {String} The HTML fragment
460   * @member Ext.XTemplate
461   * @method apply
462   */
463 Ext.XTemplate.prototype.apply = Ext.XTemplate.prototype.applyTemplate;
464
465 if (Ext.util == undefined) {
466     Ext.util = {
467         Format: {}
468     };
469 }