commit extjs-2.2.1
[extjs.git] / source / util / XTemplate.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /**\r
10 * @class Ext.XTemplate\r
11 * @extends Ext.Template\r
12 * <p>A template class that supports advanced functionality like autofilling arrays, conditional processing with\r
13 * basic comparison operators, sub-templates, basic math function support, special built-in template variables,\r
14 * inline code execution and more.  XTemplate also provides the templating mechanism built into {@link Ext.DataView}.</p>\r
15 * <p>XTemplate supports many special tags and built-in operators that aren't defined as part of the API, but are\r
16 * supported in the templates that can be created.  The following examples demonstrate all of the supported features.\r
17 * This is the data object used for reference in each code example:</p>\r
18 * <pre><code>\r
19 var data = {\r
20     name: 'Jack Slocum',\r
21     title: 'Lead Developer',\r
22     company: 'Ext JS, LLC',\r
23     email: 'jack@extjs.com',\r
24     address: '4 Red Bulls Drive',\r
25     city: 'Cleveland',\r
26     state: 'Ohio',\r
27     zip: '44102',\r
28     drinks: ['Red Bull', 'Coffee', 'Water'],\r
29     kids: [{\r
30         name: 'Sara Grace',\r
31         age:3\r
32     },{\r
33         name: 'Zachary',\r
34         age:2\r
35     },{\r
36         name: 'John James',\r
37         age:0\r
38     }]\r
39 };\r
40 </code></pre>\r
41 * <p><b>Auto filling of arrays and scope switching</b><br/>Using the <tt>tpl</tt> tag and the <tt>for</tt> operator,\r
42 * you can switch to the scope of the object specified by <tt>for</tt> and access its members to populate the template.\r
43 * If the variable in <tt>for</tt> is an array, it will auto-fill, repeating the template block inside the <tt>tpl</tt>\r
44 * tag for each item in the array:</p>\r
45 * <pre><code>\r
46 var tpl = new Ext.XTemplate(\r
47     '&lt;p>Name: {name}&lt;/p>',\r
48     '&lt;p>Title: {title}&lt;/p>',\r
49     '&lt;p>Company: {company}&lt;/p>',\r
50     '&lt;p>Kids: ',\r
51     '&lt;tpl for="kids">',\r
52         '&lt;p>{name}&lt;/p>',\r
53     '&lt;/tpl>&lt;/p>'\r
54 );\r
55 tpl.overwrite(panel.body, data);\r
56 </code></pre>\r
57 * <p><b>Access to parent object from within sub-template scope</b><br/>When processing a sub-template, for example while\r
58 * looping through a child array, you can access the parent object's members via the <tt>parent</tt> object:</p>\r
59 * <pre><code>\r
60 var tpl = new Ext.XTemplate(\r
61     '&lt;p>Name: {name}&lt;/p>',\r
62     '&lt;p>Kids: ',\r
63     '&lt;tpl for="kids">',\r
64         '&lt;tpl if="age &amp;gt; 1">',  // <-- Note that the &gt; is encoded\r
65             '&lt;p>{name}&lt;/p>',\r
66             '&lt;p>Dad: {parent.name}&lt;/p>',\r
67         '&lt;/tpl>',\r
68     '&lt;/tpl>&lt;/p>'\r
69 );\r
70 tpl.overwrite(panel.body, data);\r
71 </code></pre>\r
72 * <p><b>Array item index and basic math support</b> <br/>While processing an array, the special variable <tt>{#}</tt>\r
73 * will provide the current array index + 1 (starts at 1, not 0). Templates also support the basic math operators\r
74 * + - * and / that can be applied directly on numeric data values:</p>\r
75 * <pre><code>\r
76 var tpl = new Ext.XTemplate(\r
77     '&lt;p>Name: {name}&lt;/p>',\r
78     '&lt;p>Kids: ',\r
79     '&lt;tpl for="kids">',\r
80         '&lt;tpl if="age &amp;gt; 1">',  // <-- Note that the &gt; is encoded\r
81             '&lt;p>{#}: {name}&lt;/p>',  // <-- Auto-number each item\r
82             '&lt;p>In 5 Years: {age+5}&lt;/p>',  // <-- Basic math\r
83             '&lt;p>Dad: {parent.name}&lt;/p>',\r
84         '&lt;/tpl>',\r
85     '&lt;/tpl>&lt;/p>'\r
86 );\r
87 tpl.overwrite(panel.body, data);\r
88 </code></pre>\r
89 * <p><b>Auto-rendering of flat arrays</b> <br/>Flat arrays that contain values (and not objects) can be auto-rendered\r
90 * using the special <tt>{.}</tt> variable inside a loop.  This variable will represent the value of\r
91 * the array at the current index:</p>\r
92 * <pre><code>\r
93 var tpl = new Ext.XTemplate(\r
94     '&lt;p>{name}\'s favorite beverages:&lt;/p>',\r
95     '&lt;tpl for="drinks">',\r
96        '&lt;div> - {.}&lt;/div>',\r
97     '&lt;/tpl>'\r
98 );\r
99 tpl.overwrite(panel.body, data);\r
100 </code></pre>\r
101 * <p><b>Basic conditional logic</b> <br/>Using the <tt>tpl</tt> tag and the <tt>if</tt>\r
102 * operator you can provide conditional checks for deciding whether or not to render specific parts of the template.\r
103 * Note that there is no <tt>else</tt> operator &mdash; if needed, you should use two opposite <tt>if</tt> statements.\r
104 * Properly-encoded attributes are required as seen in the following example:</p>\r
105 * <pre><code>\r
106 var tpl = new Ext.XTemplate(\r
107     '&lt;p>Name: {name}&lt;/p>',\r
108     '&lt;p>Kids: ',\r
109     '&lt;tpl for="kids">',\r
110         '&lt;tpl if="age &amp;gt; 1">',  // <-- Note that the &gt; is encoded\r
111             '&lt;p>{name}&lt;/p>',\r
112         '&lt;/tpl>',\r
113     '&lt;/tpl>&lt;/p>'\r
114 );\r
115 tpl.overwrite(panel.body, data);\r
116 </code></pre>\r
117 * <p><b>Ability to execute arbitrary inline code</b> <br/>In an XTemplate, anything between {[ ... ]}  is considered\r
118 * code to be executed in the scope of the template. There are some special variables available in that code:\r
119 * <ul>\r
120 * <li><b><tt>values</tt></b>: The values in the current scope. If you are using scope changing sub-templates, you\r
121 * can change what <tt>values</tt> is.</li>\r
122 * <li><b><tt>parent</tt></b>: The scope (values) of the ancestor template.</li>\r
123 * <li><b><tt>xindex</tt></b>: If you are in a looping template, the index of the loop you are in (1-based).</li>\r
124 * <li><b><tt>xcount</tt></b>: If you are in a looping template, the total length of the array you are looping.</li>\r
125 * <li><b><tt>fm</tt></b>: An alias for <tt>Ext.util.Format</tt>.</li>\r
126 * </ul>\r
127 * This example demonstrates basic row striping using an inline code block and the <tt>xindex</tt> variable:</p>\r
128 * <pre><code>\r
129 var tpl = new Ext.XTemplate(\r
130     '&lt;p>Name: {name}&lt;/p>',\r
131     '&lt;p>Company: {[values.company.toUpperCase() + ", " + values.title]}&lt;/p>',\r
132     '&lt;p>Kids: ',\r
133     '&lt;tpl for="kids">',\r
134        '&lt;div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',\r
135         '{name}',\r
136         '&lt;/div>',\r
137     '&lt;/tpl>&lt;/p>'\r
138 );\r
139 tpl.overwrite(panel.body, data);\r
140 </code></pre>\r
141 * <p><b>Template member functions</b> <br/>One or more member functions can be defined directly on the config\r
142 * object passed into the XTemplate constructor for more complex processing:</p>\r
143 * <pre><code>\r
144 var tpl = new Ext.XTemplate(\r
145     '&lt;p>Name: {name}&lt;/p>',\r
146     '&lt;p>Kids: ',\r
147     '&lt;tpl for="kids">',\r
148         '&lt;tpl if="this.isGirl(name)">',\r
149             '&lt;p>Girl: {name} - {age}&lt;/p>',\r
150         '&lt;/tpl>',\r
151         '&lt;tpl if="this.isGirl(name) == false">',\r
152             '&lt;p>Boy: {name} - {age}&lt;/p>',\r
153         '&lt;/tpl>',\r
154         '&lt;tpl if="this.isBaby(age)">',\r
155             '&lt;p>{name} is a baby!&lt;/p>',\r
156         '&lt;/tpl>',\r
157     '&lt;/tpl>&lt;/p>', {\r
158      isGirl: function(name){\r
159          return name == 'Sara Grace';\r
160      },\r
161      isBaby: function(age){\r
162         return age < 1;\r
163      }\r
164 });\r
165 tpl.overwrite(panel.body, data);\r
166 </code></pre>\r
167 * @constructor\r
168 * @param {String/Array/Object} parts The HTML fragment or an array of fragments to join(""), or multiple arguments\r
169 * to join("") that can also include a config object\r
170 */\r
171 Ext.XTemplate = function(){\r
172     Ext.XTemplate.superclass.constructor.apply(this, arguments);\r
173     var s = this.html;\r
174 \r
175     s = ['<tpl>', s, '</tpl>'].join('');\r
176 \r
177     var re = /<tpl\b[^>]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/;\r
178 \r
179     var nameRe = /^<tpl\b[^>]*?for="(.*?)"/;\r
180     var ifRe = /^<tpl\b[^>]*?if="(.*?)"/;\r
181     var execRe = /^<tpl\b[^>]*?exec="(.*?)"/;\r
182     var m, id = 0;\r
183     var tpls = [];\r
184 \r
185     while(m = s.match(re)){\r
186        var m2 = m[0].match(nameRe);\r
187        var m3 = m[0].match(ifRe);\r
188        var m4 = m[0].match(execRe);\r
189        var exp = null, fn = null, exec = null;\r
190        var name = m2 && m2[1] ? m2[1] : '';\r
191        if(m3){\r
192            exp = m3 && m3[1] ? m3[1] : null;\r
193            if(exp){\r
194                fn = new Function('values', 'parent', 'xindex', 'xcount', 'with(values){ return '+(Ext.util.Format.htmlDecode(exp))+'; }');\r
195            }\r
196        }\r
197        if(m4){\r
198            exp = m4 && m4[1] ? m4[1] : null;\r
199            if(exp){\r
200                exec = new Function('values', 'parent', 'xindex', 'xcount', 'with(values){ '+(Ext.util.Format.htmlDecode(exp))+'; }');\r
201            }\r
202        }\r
203        if(name){\r
204            switch(name){\r
205                case '.': name = new Function('values', 'parent', 'with(values){ return values; }'); break;\r
206                case '..': name = new Function('values', 'parent', 'with(values){ return parent; }'); break;\r
207                default: name = new Function('values', 'parent', 'with(values){ return '+name+'; }');\r
208            }\r
209        }\r
210        tpls.push({\r
211             id: id,\r
212             target: name,\r
213             exec: exec,\r
214             test: fn,\r
215             body: m[1]||''\r
216         });\r
217        s = s.replace(m[0], '{xtpl'+ id + '}');\r
218        ++id;\r
219     }\r
220     for(var i = tpls.length-1; i >= 0; --i){\r
221         this.compileTpl(tpls[i]);\r
222     }\r
223     this.master = tpls[tpls.length-1];\r
224     this.tpls = tpls;\r
225 };\r
226 Ext.extend(Ext.XTemplate, Ext.Template, {\r
227     // private\r
228     re : /\{([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\\]\s?[\d\.\+\-\*\\\(\)]+)?\}/g,\r
229     // private\r
230     codeRe : /\{\[((?:\\\]|.|\n)*?)\]\}/g,\r
231 \r
232     // private\r
233     applySubTemplate : function(id, values, parent, xindex, xcount){\r
234         var t = this.tpls[id];\r
235         if(t.test && !t.test.call(this, values, parent, xindex, xcount)){\r
236             return '';\r
237         }\r
238         if(t.exec && t.exec.call(this, values, parent, xindex, xcount)){\r
239             return '';\r
240         }\r
241         var vs = t.target ? t.target.call(this, values, parent) : values;\r
242         parent = t.target ? values : parent;\r
243         if(t.target && Ext.isArray(vs)){\r
244             var buf = [];\r
245             for(var i = 0, len = vs.length; i < len; i++){\r
246                 buf[buf.length] = t.compiled.call(this, vs[i], parent, i+1, len);\r
247             }\r
248             return buf.join('');\r
249         }\r
250         return t.compiled.call(this, vs, parent, xindex, xcount);\r
251     },\r
252 \r
253     // private\r
254     compileTpl : function(tpl){\r
255         var fm = Ext.util.Format;\r
256         var useF = this.disableFormats !== true;\r
257         var sep = Ext.isGecko ? "+" : ",";\r
258         var fn = function(m, name, format, args, math){\r
259             if(name.substr(0, 4) == 'xtpl'){\r
260                 return "'"+ sep +'this.applySubTemplate('+name.substr(4)+', values, parent, xindex, xcount)'+sep+"'";\r
261             }\r
262             var v;\r
263             if(name === '.'){\r
264                 v = 'values';\r
265             }else if(name === '#'){\r
266                 v = 'xindex';\r
267             }else if(name.indexOf('.') != -1){\r
268                 v = name;\r
269             }else{\r
270                 v = "values['" + name + "']";\r
271             }\r
272             if(math){\r
273                 v = '(' + v + math + ')';\r
274             }\r
275             if(format && useF){\r
276                 args = args ? ',' + args : "";\r
277                 if(format.substr(0, 5) != "this."){\r
278                     format = "fm." + format + '(';\r
279                 }else{\r
280                     format = 'this.call("'+ format.substr(5) + '", ';\r
281                     args = ", values";\r
282                 }\r
283             }else{\r
284                 args= ''; format = "("+v+" === undefined ? '' : ";\r
285             }\r
286             return "'"+ sep + format + v + args + ")"+sep+"'";\r
287         };\r
288         var codeFn = function(m, code){\r
289             return "'"+ sep +'('+code+')'+sep+"'";\r
290         };\r
291 \r
292         var body;\r
293         // branched to use + in gecko and [].join() in others\r
294         if(Ext.isGecko){\r
295             body = "tpl.compiled = function(values, parent, xindex, xcount){ return '" +\r
296                    tpl.body.replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.re, fn).replace(this.codeRe, codeFn) +\r
297                     "';};";\r
298         }else{\r
299             body = ["tpl.compiled = function(values, parent, xindex, xcount){ return ['"];\r
300             body.push(tpl.body.replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.re, fn).replace(this.codeRe, codeFn));\r
301             body.push("'].join('');};");\r
302             body = body.join('');\r
303         }\r
304         eval(body);\r
305         return this;\r
306     },\r
307 \r
308     /**\r
309      * Returns an HTML fragment of this template with the specified values applied.\r
310      * @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'})\r
311      * @return {String} The HTML fragment\r
312      */\r
313     applyTemplate : function(values){\r
314         return this.master.compiled.call(this, values, {}, 1, 1);\r
315     },\r
316 \r
317     /**\r
318      * Compile the template to a function for optimized performance.  Recommended if the template will be used frequently.\r
319      * @return {Function} The compiled function\r
320      */\r
321     compile : function(){return this;}\r
322 \r
323     /**\r
324      * @property re\r
325      * @hide\r
326      */\r
327     /**\r
328      * @property disableFormats\r
329      * @hide\r
330      */\r
331     /**\r
332      * @method set\r
333      * @hide\r
334      */\r
335 \r
336 });\r
337 /**\r
338  * Alias for {@link #applyTemplate}\r
339  * Returns an HTML fragment of this template with the specified values applied.\r
340  * @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'})\r
341  * @return {String} The HTML fragment\r
342  * @member Ext.XTemplate\r
343  * @method apply\r
344  */\r
345 Ext.XTemplate.prototype.apply = Ext.XTemplate.prototype.applyTemplate;\r
346 \r
347 /**\r
348  * Creates a template from the passed element's value (<i>display:none</i> textarea, preferred) or innerHTML.\r
349  * @param {String/HTMLElement} el A DOM element or its id\r
350  * @return {Ext.Template} The created template\r
351  * @static\r
352  */\r
353 Ext.XTemplate.from = function(el){\r
354     el = Ext.getDom(el);\r
355     return new Ext.XTemplate(el.value || el.innerHTML);\r
356 };