]>
Commit | Line | Data |
---|---|---|
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 | |
150 | Ext.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 |