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