/** * This class manages a formula defined for an `Ext.app.ViewModel`. * * ## Formula Basics * * Formulas in a `ViewModel` can be defined as simply as just a function: * * formulas: { * xy: function (get) { return get('x') * get('y'); } * } * * When you need to be more explicit, "xy" can become an object. The following means the * same thing as above: * * formulas: { * xy: { * get: function (get) { return get('x') * get('y'); } * } * } * * ### Data Dependencies * * One of the important aspects of a `ViewModel` is notification of change. In order to * manage this, a `ViewModel` *must* know the dependencies between data. In the above case * this is accomplished by **parsing the text of the function**. While this is convenient * and reduces the maintenance/risk that would come from explicitly listing dependencies * separately, there are some rules to be aware of: * * * All dependencies are resolved by matching the binding statements in the getter function. * * If you need to use these values in other ways, cache them as a `var` (following * the first rule to capture the value) and use that `var`. * * In the above formulas, the "xy" formula depends on "x" and "y" in the `ViewModel`. As * these values change, the formula is called to produce the correct value for "xy". This * in turn can be used by other formulas. For example: * * formulas: { * xy: function (get) { // "get" is arbitrary but a good convention * return get('x') * get('y'); * }, * * xyz: function (get) { * return get('xy') * get('z'); * } * } * * In the above, "xyz" depends on "xy" and "z" values in the `ViewModel`. * * ### The Getter Method * * The argument passed to the formula is a function that allows you to retrieve * the matched bind statements. * * formulas: { * foo: function (get) { * return get('theUser.address.city'); * } * } * * In the above, the dependency is resolved to `theUser.address.city`. The formula will not * be triggered until the value for `city` is present. * * ### Capturing Values * * If values need to be used repeatedly, you can use a `var` as long as the Rules are not * broken. * * formulas: { * x2y2: function (get) { * // These are still "visible" as "get('x')" and "get('y')" so this is OK: * var x = get('x'), * y = get('y'); * * return x * x * y * y; * } * } * * ## Explicit Binding * * While function parsing is convenient, there are times it is not the best solution. In * these cases, an explicit `bind` can be given. To revisit the previous example with an * explicit binding: * * formulas: { * zip: { * bind: '{foo.bar.zip}', * * get: function (zip) { * // NOTE: the only thing we get is what our bind produces. * return zip * 2; * } * } * } * * In this case we have given the formula an explicit `bind` value so it will no longer * parse the `get` function. Instead, it will call `{@link Ext.app.ViewModel#bind}` with * the value of the `bind` property and pass the produced value to `get` whenever it * changes. * * ## Settable Formulas * * When a formula is "reversible" it can be given a `set` method to allow it to participate * in two-way binding. For example: * * formulas: { * fullName: { * get: function (get) { * var ret = get('firstName') || ''; * * if (get('lastName')) { * ret += ' ' + get('lastName'); * } * * return ret; * }, * * set: function (value) { * var space = value.indexOf(' '), * split = (space < 0) ? value.length : space; * * this.set({ * firstName: value.substring(0, split), * lastName: value.substring(split + 1) * }); * } * } * } * * When the `set` method is called the `this` reference is the `Ext.app.ViewModel` so it * just calls its `{@link Ext.app.ViewModel#method-set set method}`. * * ## Single Run Formulas * * If a formula only needs to produce an initial value, it can be marked as `single`. * * formulas: { * xy: { * single: true, * * get: function (get) { * return get('x') * get('y'); * } * } * } * * This formulas `get` method will be called with `x` and `y` once and then its binding * to these properties will be destroyed. This means the `get` method (and hence the value * of `xy`) will only be executed/calculated once. */Ext.define('Ext.app.bind.Formula', { extend: 'Ext.util.Schedulable', requires: [ 'Ext.util.LruCache' ], statics: { getFormulaParser: function(name) { var cache = this.formulaCache, parser, s; if (!cache) { cache = this.formulaCache = new Ext.util.LruCache({ maxSize: 20 }); } parser = cache.get(name); if (!parser) { // Unescaped: [^\.a-z0-9_]NAMEHERE\(\s*(['"])(.*?)\1\s*\) s = '[^\\.a-z0-9_]' + Ext.String.escapeRegex(name) + '\\(\\s*([\'"])(.*?)\\1\\s*\\)'; parser = new RegExp(s, 'gi'); cache.add(name, parser); } return parser; } }, isFormula: true, calculation: null, explicit: false, /** * @cfg {Object} [bind] * An explicit bind request to produce data to provide the `get` function. If this is * specified, the result of this bind is the first argument to `get`. If not given, * then `get` receives a getter function that can retrieve bind expressions. For details * on what can be specified for this property see `{@link Ext.app.ViewModel#bind}`. * @since 5.0.0 */ /** * @cfg {Function} get * The function to call to calculate the formula's value. The `get` method executes * with a `this` pointer of the `ViewModel` and receives a getter function or the result of * a configured `bind`. * @since 5.0.0 */ /** * @cfg {Function} [set] * If provided this method allows a formula to be set. This method is typically called * when `{@link Ext.app.bind.Binding#setValue}` is called. The `set` method executes * with a `this` pointer of the `ViewModel`. Whatever values need to be updated can * be set by calling `{@link Ext.app.ViewModel#set}`. * @since 5.0.0 */ set: null, /** * @cfg {Boolean} [single=false] * This option instructs the binding to call its `destroy` method immediately after * delivering the initial value. * @since 5.0.0 */ single: false, /* eslint-disable-next-line no-useless-escape */ fnKeywordArgumentNamesRe: /^function\s*[^\(]*\(\s*([^,\)\s]+)/, fnKeywordRe: /^\s*function/, /* eslint-disable-next-line no-useless-escape */ replaceParenRe: /[\(\)]/g, constructor: function(stub, formula) { var me = this, owner = stub.owner, bindTo, expressions, getter, options; me.owner = owner; me.stub = stub; me.callParent(); if (formula instanceof Function) { me.get = getter = formula; } else { me.get = getter = formula.get; me.set = formula.set; expressions = formula.bind; if (formula.single) { me.single = formula.single; } if (expressions) { bindTo = expressions.bindTo; if (bindTo) { options = Ext.apply({}, expressions); delete options.bindTo; expressions = bindTo; } } } //<debug> if (!getter) { Ext.raise('Must specify a getter method for a formula'); } //</debug> if (expressions) { me.explicit = true; } else { expressions = getter.$expressions || me.parseFormula(getter); } me.binding = owner.bind(expressions, me.onChange, me, options); }, destroy: function() { var me = this, binding = me.binding, stub = me.stub; if (binding) { binding.destroy(); me.binding = null; } if (stub) { stub.formula = null; } me.callParent(); // Save for last because this is used to remove us from the Scheduler me.getterFn = me.owner = null; }, getFullName: function() { return this.fullName || (this.fullName = this.stub.getFullName() + '=' + this.callParent() + ')'); }, getRawValue: function() { return this.calculation; }, onChange: function() { if (!this.scheduled) { this.schedule(); } }, parseFormula: function(formula) { var str = Ext.Function.toCode(formula), defaultProp = 'get', expressions = { $literal: true }, match, getterProp, formulaRe, expr; if (this.fnKeywordRe.test(str)) { match = this.fnKeywordArgumentNamesRe.exec(str); if (match) { getterProp = match[1]; } } else { match = str.split('=>')[0]; if (match) { match = Ext.String.trim(match.replace(this.replaceParenRe, '')).split(','); getterProp = match[0]; } } getterProp = getterProp || defaultProp; formulaRe = Ext.app.bind.Formula.getFormulaParser(getterProp); while ((match = formulaRe.exec(str))) { expr = match[2]; expressions[expr] = expr; } expressions.$literal = true; // We store the parse results on the function object because we might reuse the // formula function (typically when a ViewModel class is created a 2nd+ time). formula.$expressions = expressions; return expressions; }, react: function() { var me = this, owner = me.owner, data = me.binding.lastValue, arg; if (me.explicit) { arg = data; } else { arg = owner.getFormulaFn(data); } me.settingValue = true; me.stub.set(me.calculation = me.get.call(owner, arg)); me.settingValue = false; if (me.single) { me.destroy(); } }, setValue: function(value) { this.set.call(this.stub.owner, value); }, privates: { getScheduler: function() { var owner = this.owner; return owner && owner.getScheduler(); }, sort: function() { var me = this, binding = me.binding; // Our binding may be single:true if (!binding.destroyed) { me.scheduler.sortItem(binding); } // Schedulable#sort === emptyFn // me.callParent(); } }});