]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/app/bind/Formula.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / app / bind / Formula.js
CommitLineData
6527f429
DM
1/**\r
2 * This class manages a formula defined for an `Ext.app.ViewModel`.\r
3 *\r
4 * ## Formula Basics\r
5 *\r
6 * Formulas in a `ViewModel` can be defined as simply as just a function:\r
7 *\r
8 * formulas: {\r
9 * xy: function (get) { return get('x') * get('y'); }\r
10 * }\r
11 *\r
12 * When you need to be more explicit, "xy" can become an object. The following means the\r
13 * same thing as above:\r
14 *\r
15 * formulas: {\r
16 * xy: {\r
17 * get: function (get) { return get('x') * get('y'); }\r
18 * }\r
19 * }\r
20 *\r
21 * ### Data Dependencies\r
22 *\r
23 * One of the important aspects of a `ViewModel` is notification of change. In order to\r
24 * manage this, a `ViewModel` *must* know the dependencies between data. In the above case\r
25 * this is accomplished by **parsing the text of the function**. While this is convenient\r
26 * and reduces the maintenance/risk that would come from explicitly listing dependencies\r
27 * separately, there are some rules to be aware of:\r
28 *\r
29 * * All dependencies are resolved by matching the binding statements in the getter function.\r
30 * * If you need to use these values in other ways, cache them as a `var` (following\r
31 * the first rule to capture the value) and use that `var`.\r
32 *\r
33 * In the above formulas, the "xy" formula depends on "x" and "y" in the `ViewModel`. As\r
34 * these values change, the formula is called to produce the correct value for "xy". This\r
35 * in turn can be used by other formulas. For example:\r
36 *\r
37 * formulas: {\r
38 * xy: function (get) { // "get" is arbitrary but a good convention\r
39 * return get('x') * get('y');\r
40 * },\r
41 *\r
42 * xyz: function (get) {\r
43 * return get('xy') * get('z');\r
44 * }\r
45 * }\r
46 *\r
47 * In the above, "xyz" depends on "xy" and "z" values in the `ViewModel`.\r
48 *\r
49 * ### The Getter Method\r
50 *\r
51 * The argument passed to the formula is a function that allows you to retrieve\r
52 * the matched bind statements.\r
53 *\r
54 * formulas: {\r
55 * foo: function (get) {\r
56 * return get('theUser.address.city');\r
57 * }\r
58 * }\r
59 *\r
60 * In the above, the dependency is resolved to `theUser.address.city`. The formula will not\r
61 * be triggered until the value for `city` is present.\r
62 *\r
63 * ### Capturing Values\r
64 *\r
65 * If values need to be used repeatedly, you can use a `var` as long as the Rules are not\r
66 * broken.\r
67 *\r
68 * formulas: {\r
69 * x2y2: function (get) {\r
70 * // These are still "visible" as "get('x')" and "get('y')" so this is OK:\r
71 * var x = get('x'),\r
72 * y = get('y');\r
73 *\r
74 * return x * x * y * y;\r
75 * }\r
76 * }\r
77 *\r
78 * ## Explicit Binding\r
79 *\r
80 * While function parsing is convenient, there are times it is not the best solution. In\r
81 * these cases, an explicit `bind` can be given. To revisit the previous example with an\r
82 * explicit binding:\r
83 *\r
84 * formulas: {\r
85 * zip: {\r
86 * bind: '{foo.bar.zip}',\r
87 *\r
88 * get: function (zip) {\r
89 * // NOTE: the only thing we get is what our bind produces.\r
90 * return zip * 2;\r
91 * }\r
92 * }\r
93 * }\r
94 *\r
95 * In this case we have given the formula an explicit `bind` value so it will no longer\r
96 * parse the `get` function. Instead, it will call `{@link Ext.app.ViewModel#bind}` with\r
97 * the value of the `bind` property and pass the produced value to `get` whenever it\r
98 * changes.\r
99 *\r
100 * ## Settable Formulas\r
101 *\r
102 * When a formula is "reversible" it can be given a `set` method to allow it to participate\r
103 * in two-way binding. For example:\r
104 *\r
105 * formulas: {\r
106 * fullName: {\r
107 * get: function (get) {\r
108 * var ret = get('firstName') || '';\r
109 *\r
110 * if (get('lastName')) {\r
111 * ret += ' ' + get('lastName');\r
112 * }\r
113 *\r
114 * return ret;\r
115 * },\r
116 *\r
117 * set: function (value) {\r
118 * var space = value.indexOf(' '),\r
119 * split = (space < 0) ? value.length : space;\r
120 *\r
121 * this.set({\r
122 * firstName: value.substring(0, split),\r
123 * lastName: value.substring(split + 1)\r
124 * });\r
125 * }\r
126 * }\r
127 * }\r
128 *\r
129 * When the `set` method is called the `this` reference is the `Ext.app.ViewModel` so it\r
130 * just calls its `{@link Ext.app.ViewModel#method-set set method}`.\r
131 *\r
132 * ## Single Run Formulas\r
133 *\r
134 * If a formula only needs to produce an initial value, it can be marked as `single`.\r
135 *\r
136 * formulas: {\r
137 * xy: {\r
138 * single: true,\r
139 *\r
140 * get: function (get) {\r
141 * return get('x') * get('y');\r
142 * }\r
143 * }\r
144 * }\r
145 *\r
146 * This formulas `get` method will be called with `x` and `y` once and then its binding\r
147 * to these properties will be destroyed. This means the `get` method (and hence the value\r
148 * of `xy`) will only be executed/calculated once.\r
149 */\r
150Ext.define('Ext.app.bind.Formula', {\r
151 extend: 'Ext.util.Schedulable',\r
152\r
153 requires: [\r
154 'Ext.util.LruCache'\r
155 ],\r
156\r
157 statics: {\r
158 getFormulaParser: function(name) {\r
159 var cache = this.formulaCache,\r
160 parser, s;\r
161\r
162 if (!cache) {\r
163 cache = this.formulaCache = new Ext.util.LruCache({\r
164 maxSize: 20\r
165 });\r
166 }\r
167\r
168 parser = cache.get(name);\r
169 if (!parser) {\r
170 // Unescaped: [^\.a-z0-9_]NAMEHERE\(\s*(['"])(.*?)\1\s*\)\r
171 s = '[^\\.a-z0-9_]' + name + '\\(\\s*([\'"])(.*?)\\1\\s*\\)';\r
172 parser = new RegExp(s, 'gi');\r
173 cache.add(name, parser);\r
174 }\r
175 return parser;\r
176 }\r
177 },\r
178\r
179 isFormula: true,\r
180\r
181 calculation: null,\r
182\r
183 explicit: false,\r
184\r
185 /**\r
186 * @cfg {Object} [bind]\r
187 * An explicit bind request to produce data to provide the `get` function. If this is\r
188 * specified, the result of this bind is the first argument to `get`. If not given,\r
189 * then `get` receives a getter function that can retrieve bind expressions. For details on what can\r
190 * be specified for this property see `{@link Ext.app.ViewModel#bind}`.\r
191 * @since 5.0.0\r
192 */\r
193\r
194 /**\r
195 * @cfg {Function} get\r
196 * The function to call to calculate the formula's value. The `get` method executes\r
197 * with a `this` pointer of the `ViewModel` and receives a getter function or the result of a configured `bind`.\r
198 * @since 5.0.0\r
199 */\r
200\r
201 /**\r
202 * @cfg {Function} [set]\r
203 * If provided this method allows a formula to be set. This method is typically called\r
204 * when `{@link Ext.app.bind.Binding#setValue}` is called. The `set` method executes\r
205 * with a `this` pointer of the `ViewModel`. Whatever values need to be updated can\r
206 * be set by calling `{@link Ext.app.ViewModel#set}`.\r
207 * @since 5.0.0\r
208 */\r
209 set: null,\r
210\r
211 /**\r
212 * @cfg {Boolean} [single=false]\r
213 * This option instructs the binding to call its `destroy` method immediately after\r
214 * delivering the initial value.\r
215 * @since 5.0.0\r
216 */\r
217 single: false,\r
218\r
219 argumentNamesRe: /^function\s*\(\s*([^,\)\s]+)/,\r
220\r
221 constructor: function (stub, formula) {\r
222 var me = this,\r
223 owner = stub.owner,\r
224 bindTo, expressions, getter, options;\r
225\r
226 me.owner = owner;\r
227 me.stub = stub;\r
228\r
229 me.callParent();\r
230\r
231 if (formula instanceof Function) {\r
232 me.get = getter = formula;\r
233 } else {\r
234 me.get = getter = formula.get;\r
235 me.set = formula.set;\r
236 expressions = formula.bind;\r
237\r
238 if (formula.single) {\r
239 me.single = formula.single;\r
240 }\r
241\r
242 if (expressions) {\r
243 bindTo = expressions.bindTo;\r
244\r
245 if (bindTo) {\r
246 options = Ext.apply({}, expressions);\r
247 delete options.bindTo;\r
248 expressions = bindTo;\r
249 }\r
250 }\r
251 }\r
252\r
253 //<debug>\r
254 if (!getter) {\r
255 Ext.raise('Must specify a getter method for a formula');\r
256 }\r
257 //</debug>\r
258\r
259 if (expressions) {\r
260 me.explicit = true;\r
261 } else {\r
262 expressions = getter.$expressions || me.parseFormula(getter);\r
263 }\r
264\r
265 me.binding = owner.bind(expressions, me.onChange, me, options);\r
266 },\r
267\r
268 destroy: function () {\r
269 var me = this,\r
270 binding = me.binding,\r
271 stub = me.stub;\r
272\r
273 if (binding) {\r
274 binding.destroy();\r
275 me.binding = null;\r
276 }\r
277\r
278 if (stub) {\r
279 stub.formula = null;\r
280 }\r
281\r
282 me.callParent();\r
283\r
284 // Save for last because this is used to remove us from the Scheduler\r
285 me.getterFn = me.owner = null;\r
286 },\r
287\r
288 getFullName: function () {\r
289 return this.fullName ||\r
290 (this.fullName = this.stub.getFullName() + '=' + this.callParent() + ')');\r
291 },\r
292\r
293 getRawValue: function () {\r
294 return this.calculation;\r
295 },\r
296\r
297 onChange: function () {\r
298 if (!this.scheduled) {\r
299 this.schedule();\r
300 }\r
301 },\r
302\r
303 parseFormula: function (formula) {\r
304 var str = formula.toString(),\r
305 expressions = {\r
306 $literal: true\r
307 },\r
308 match, getterProp, formulaRe, expr;\r
309\r
310 match = this.argumentNamesRe.exec(str);\r
311 getterProp = match ? match[1] : 'get';\r
312 formulaRe = Ext.app.bind.Formula.getFormulaParser(getterProp);\r
313\r
314 while ((match = formulaRe.exec(str))) {\r
315 expr = match[2];\r
316 expressions[expr] = expr;\r
317 }\r
318\r
319 expressions.$literal = true;\r
320\r
321 // We store the parse results on the function object because we might reuse the\r
322 // formula function (typically when a ViewModel class is created a 2nd+ time).\r
323 formula.$expressions = expressions;\r
324\r
325 return expressions;\r
326 },\r
327\r
328 react: function () {\r
329 var me = this,\r
330 owner = me.owner,\r
331 data = me.binding.lastValue,\r
332 getterFn = me.getterFn,\r
333 arg;\r
334\r
335 if (me.explicit) {\r
336 arg = data;\r
337 } else {\r
338 arg = owner.getFormulaFn(data);\r
339 }\r
340 me.settingValue = true;\r
341 me.stub.set(me.calculation = me.get.call(owner, arg));\r
342 me.settingValue = false;\r
343\r
344 if (me.single) {\r
345 me.destroy();\r
346 }\r
347 },\r
348\r
349 setValue: function(value) {\r
350 this.set.call(this.stub.owner, value);\r
351 },\r
352\r
353 privates: {\r
354 getScheduler: function () {\r
355 var owner = this.owner;\r
356 return owner && owner.getScheduler();\r
357 },\r
358 \r
359 sort: function () {\r
360 var me = this,\r
361 binding = me.binding;\r
362\r
363 // Our binding may be single:true\r
364 if (!binding.destroyed) {\r
365 me.scheduler.sortItem(binding);\r
366 }\r
367\r
368 // Schedulable#sort === emptyFn\r
369 //me.callParent();\r
370 }\r
371 }\r
372});\r