/** * This class is intended as a mixin for classes that want to provide a "bind" config that * connects to a `ViewModel`. * @private * @since 5.0.0 */Ext.define('Ext.mixin.Bindable', { mixinId: 'bindable', config: { /** * @cfg {Object/String} [bind] * Setting this config option adds or removes data bindings for other configs. * For example, to bind the `title` config: * * var panel = Ext.create({ * xtype: 'panel', * bind: { * title: 'Hello {user.name}' * } * }); * * To dynamically add bindings: * * panel.setBind({ * title: 'Greetings {user.name}!' * }); * * To remove bindings: * * panel.setBind({ * title: null * }); * * The bind expressions are presented to `{@link Ext.app.ViewModel#bind}`. The * `ViewModel` instance is determined by `lookupViewModel`. * * **Note:** If bind is passed as a string, it will use the {@link Ext.Component#property-defaultBindProperty} * for the binding. */ bind: { $value: null, lazy: true }, // @cmd-auto-dependency { aliasPrefix: 'controller.' } /** * @cfg {String/Object/Ext.app.ViewController} controller * A string alias, a configuration object or an instance of a `ViewController` for * this container. Sample usage: * * Ext.define('MyApp.UserController', { * alias: 'controller.user' * }); * * Ext.define('UserContainer', { * extend: 'Ext.container.container', * controller: 'user' * }); * // Or * Ext.define('UserContainer', { * extend: 'Ext.container.container', * controller: { * type: 'user', * someConfig: true * } * }); * * // Can also instance at runtime * var ctrl = new MyApp.UserController(); * var view = new UserContainer({ * controller: ctrl * }); * */ controller: null, /** * @method getController * Returns the {@link Ext.app.ViewController} instance associated with this * component via the {@link #controller} config or {@link #setController} method. * @return {Ext.app.ViewController} Returns this component's ViewController or * null if one was not configured */ /** * @cfg {Boolean} defaultListenerScope * If `true`, this component will be the default scope (this pointer) for events * specified with string names so that the scope can be dynamically resolved. The * component will automatically become the defaultListenerScope if a * {@link #controller} is specified. * * See the introductory docs for {@link Ext.container.Container} for some sample * usages. * * **NOTE**: This value can only be reliably set at construction time. Setting it * after that time may not correctly rewire all of the potentially effected * listeners. */ defaultListenerScope: false, /** * @cfg {String/String[]/Object} publishes * One or more names of config properties that this component should publish * to its ViewModel. Generally speaking, only properties defined in a class config * block (including ancestor config blocks and mixins) are eligible for publishing * to the viewModel. Some components override this and publish their most useful * configs by default. * * **Note:** We'll discuss publishing properties **not** found in the config block below. * * Values determined to be invalid by component (often form fields and model validations) * will not be published to the ViewModel. * * This config uses the `{@link #cfg-reference}` to determine the name of the data * object to place in the `ViewModel`. If `reference` is not set then this config * is ignored. * * By using this config and `{@link #cfg-reference}` you can bind configs between * components. For example: * * ... * items: [{ * xtype: 'textfield', * reference: 'somefield', // component's name in the ViewModel * publishes: 'value' // value is not published by default * },{ * ... * },{ * xtype: 'displayfield', * bind: 'You have entered "{somefield.value}"' * }] * ... * * Classes must provide this config as an Object: * * Ext.define('App.foo.Bar', { * publishes: { * foo: true, * bar: true * } * }); * * This is required for the config system to properly merge values from derived * classes. * * For instances this value can be specified as a value as show above or an array * or object as follows: * * { * xtype: 'textfield', * reference: 'somefield', * publishes: [ * 'value', * 'rawValue', * 'dirty' * ] * } * * // This achieves the same result as the above array form. * { * xtype: 'textfield', * reference: 'somefield', * publishes: { * value: true, * rawValue: true, * dirty: true * } * } * * In some cases, users may want to publish a property to the viewModel that is not found in a class * config block. In these situations, you may utilize {@link #publishState} if the property has a * setter method. Let's use {@link Ext.form.Labelable#setFieldLabel setFieldLabel} as an example: * * setFieldLabel: function(fieldLabel) { * this.callParent(arguments); * this.publishState('fieldLabel', fieldLabel); * } * * With the above chunk of code, fieldLabel may now be published to the viewModel. * * @since 5.0.0 */ publishes: { $value: null, lazy: true, merge: function (newValue, oldValue) { return this.mergeSets(newValue, oldValue); } }, /** * @cfg {String} reference * Specifies a name for this component inside its component hierarchy. This name * must be unique within its {@link Ext.container.Container#referenceHolder view} * or its {@link Ext.app.ViewController ViewController}. See the documentation in * {@link Ext.container.Container} for more information about references. * * **Note**: Valid identifiers start with a letter or underscore and are followed * by zero or more additional letters, underscores or digits. References are case * sensitive. */ reference: null, // @cmd-auto-dependency { directRef: 'Ext.data.Session' } /** * @cfg {Boolean/Object/Ext.data.Session} [session=null] * If provided this creates a new `Session` instance for this component. If this * is a `Container`, this will then be inherited by all child components. * * To create a new session you can specify `true`: * * Ext.create({ * xtype: 'viewport', * session: true, * * items: [{ * ... * }] * }); * * Alternatively, a config object can be provided: * * Ext.create({ * xtype: 'viewport', * session: { * ... * }, * * items: [{ * ... * }] * }); * */ session: { $value: null, lazy: true }, /** * @cfg {String/String[]/Object} twoWayBindable * This object holds a map of `config` properties that will update their binding * as they are modified. For example, `value` is a key added by form fields. The * form of this config is the same as `{@link #publishes}`. * * This config is defined so that updaters are not created and added for all * bound properties since most cannot be modified by the end-user and hence are * not appropriate for two-way binding. */ twoWayBindable: { $value: null, lazy: true, merge: function (newValue, oldValue) { return this.mergeSets(newValue, oldValue); } }, // @cmd-auto-dependency { aliasPrefix: 'viewmodel.', defaultType: 'default' } /** * @cfg {String/Object/Ext.app.ViewModel} viewModel * The `ViewModel` is a data provider for this component and its children. The * data contained in the `ViewModel` is typically used by adding `bind` configs * to the components that want present or edit this data. * * When set, the `ViewModel` is created and links to any inherited `viewModel` * instance from an ancestor container as the "parent". The `ViewModel` hierarchy, * once established, only supports creation or destruction of children. The * parent of a `ViewModel` cannot be changed on the fly. * * If this is a root-level `ViewModel`, the data model connection is made to this * component's associated `{@link Ext.data.Session Data Session}`. This is * determined by calling `getInheritedSession`. * */ viewModel: { $value: null, lazy: true } }, /** * @property {String} [defaultBindProperty] * This property is used to determine the property of a `bind` config that is just * the value. For example, if `defaultBindProperty="value"`, then this shorthand * `bind` config: * * bind: '{name}' * * Is equivalent to this object form: * * bind: { * value: '{name}' * } * * The `defaultBindProperty` is set to "value" for form fields and to "store" for * grids and trees. * @protected */ defaultBindProperty: null, /** * @property {RegExp} * Regular expression used for validating `reference` values. * @private */ validRefRe: /^[a-z_][a-z0-9_]*$/i, /** * Called by `getInherited` to initialize the inheritedState the first time it is * requested. * @protected */ initInheritedState: function (inheritedState) { var me = this, reference = me.getReference(), controller = me.getController(), // Don't instantiate the view model here, we only need to know that // it exists viewModel = me.getConfig('viewModel', true), session = me.getConfig('session', true), defaultListenerScope = me.getDefaultListenerScope(); if (controller) { inheritedState.controller = controller; } if (defaultListenerScope) { inheritedState.defaultListenerScope = me; } else if (controller) { inheritedState.defaultListenerScope = controller; } if (viewModel) { // If we're not configured with an instance, just stamp the current component as // the thing that holds the view model. When we ask to get the inherited view model, // we will know that it's not an instance yet so we need to spin it up on this component. // We need to initialize them from top-down, but we don't want to do it up front. if (!viewModel.isViewModel) { viewModel = me; } inheritedState.viewModel = viewModel; } // Same checks as the view model if (session) { if (!session.isSession) { session = me; } inheritedState.session = session; } if (reference) { me.referenceKey = (inheritedState.referencePath || '') + reference; me.viewModelKey = (inheritedState.viewModelPath || '') + reference; } }, /** * Gets the controller that controls this view. May be a controller that belongs * to a view higher in the hierarchy. * * @param {Boolean} [skipThis=false] `true` to not consider the controller directly attached * to this view (if it exists). * @return {Ext.app.ViewController} The controller. `null` if no controller is found. * * @since 5.0.1 */ lookupController: function(skipThis) { return this.getInheritedConfig('controller', skipThis) || null; }, /** * Returns the `Ext.data.Session` for this instance. This property may come * from this instance's `{@link #session}` or be inherited from this object's parent. * @param {Boolean} [skipThis=false] Pass `true` to ignore a `session` configured on * this instance and only consider an inherited session. * @return {Ext.data.Session} * @since 5.0.0 */ lookupSession: function (skipThis) { // See lookupViewModel var ret = skipThis ? null : this.getSession(); // may be the initGetter! if (!ret) { ret = this.getInheritedConfig('session', skipThis); if (ret && !ret.isSession) { ret = ret.getInherited().session = ret.getSession(); } } return ret || null; }, /** * Returns the `Ext.app.ViewModel` for this instance. This property may come from this * this instance's `{@link #viewModel}` or be inherited from this object's parent. * @param {Boolean} [skipThis=false] Pass `true` to ignore a `viewModel` configured on * this instance and only consider an inherited view model. * @return {Ext.app.ViewModel} * @since 5.0.0 */ lookupViewModel: function (skipThis) { var ret = skipThis ? null : this.getViewModel(); // may be the initGetter! if (!ret) { ret = this.getInheritedConfig('viewModel', skipThis); // If what we get back is a component, it means the component was configured // with a view model, however the construction of it has been delayed until // we need it. As such, go and construct it and store it on the inherited state. if (ret && !ret.isViewModel) { ret = ret.getInherited().viewModel = ret.getViewModel(); } } return ret || null; }, /** * Publish this components state to the `ViewModel`. If no arguments are given (or if * this is the first call), the entire state is published. This state is determined by * the `publishes` property. * * This method is called only by component authors. * * @param {String} [property] The name of the property to update. * @param {Object} [value] The value of `property`. Only needed if `property` is given. * @protected * @since 5.0.0 */ publishState: function (property, value) { var me = this, state = me.publishedState, binds = me.getBind(), binding = binds && property && binds[property], count = 0, name, publishes, vm, path; if (binding && !binding.syncing && !binding.isReadOnly()) { // If the binding has never fired & our value is either: // a) undefined // b) null // c) The value we were initially configured with // Then we don't want to publish it back to the view model. If we do, we'll be // overwriting whatever is in the viewmodel and it will never have a chance to fire. if (!(binding.calls === 0 && (value == null || value === me.getInitialConfig()[property]))) { binding.setValue(value); } } if (!(publishes = me.getPublishes())) { return; } if (!(vm = me.lookupViewModel())) { return; } // Important to access path after lookupViewModel, which will kick off // our inheritedState if we don't have one if (!(path = me.viewModelKey)) { return; } if (property && state) { if (!publishes[property]) { return; } // If we are setting an individual property and that is not a {} or a [] then // check to see if it is unchanged. if (!(value && value.constructor === Object) && !(value instanceof Array)) { if (state[property] === value) { return; } } path += '.'; path += property; } else { state = state || (me.publishedState = {}); for (name in publishes) { ++count; // If there are no properties to publish this loop will not run and the // value = null above will remain. if (name === property) { state[name] = value; } else { state[name] = me[name]; } } if (!count) { // if (no properties were put in "state") return; } value = state; } vm.set(path, value); }, //========================================================================= privates: { /** * Ensures that the given property (if it is a Config System config) has a proper * "updater" method on this instance to sync changes to the config. * @param {String} property The name of the config property. * @private * @since 5.0.0 */ addBindableUpdater: function (property) { var me = this, configs = me.self.$config.configs, cfg = configs[property], updateName; // While we store the updater on this instance, the function is cached and // re-used across all instances. if (cfg && !me.hasOwnProperty(updateName = cfg.names.update)) { me[updateName] = cfg.bindableUpdater || (cfg.root.bindableUpdater = me.makeBindableUpdater(cfg)); } }, /** * @param {String/Object} binds * @param {Object} currentBindings * @return {Object} * @private * @since 5.0.0 */ applyBind: function (binds, currentBindings) { if (!binds) { return binds; } var me = this, viewModel = me.lookupViewModel(), twoWayable = me.getTwoWayBindable(), getBindTemplateScope = me._getBindTemplateScope, b, property, descriptor, destroy; me.$hasBinds = true; if (!currentBindings || typeof currentBindings === 'string') { currentBindings = {}; } //<debug> if (!viewModel) { Ext.raise('Cannot use bind config without a viewModel'); } //</debug> if (Ext.isString(binds)) { //<debug> if (!me.defaultBindProperty) { Ext.raise(me.$className + ' has no defaultBindProperty - '+ 'Please specify a bind object'); } //</debug> b = binds; binds = {}; binds[me.defaultBindProperty] = b; } for (property in binds) { descriptor = binds[property]; b = currentBindings[property]; if (b && typeof b !== 'string') { b.destroy(); b = null; destroy = true; } if (descriptor) { b = viewModel.bind(descriptor, me.onBindNotify, me); b._config = Ext.Config.get(property); b.getTemplateScope = getBindTemplateScope; //<debug> if (!me[b._config.names.set]) { Ext.raise('Cannot bind ' + property + ' on ' + me.$className + ' - missing a ' + b._config.names.set + ' method.'); } //</debug> } if (destroy) { delete currentBindings[property]; } else { currentBindings[property] = b; } if (twoWayable && twoWayable[property]) { if (destroy) { me.clearBindableUpdater(property); } else if (!b.isReadOnly()) { me.addBindableUpdater(property); } } } return currentBindings; }, applyController: function (controller) { if (controller) { controller = Ext.Factory.controller(controller); controller.setView(this); } return controller; }, applyPublishes: function (all) { if (this.lookupViewModel()) { for (var property in all) { this.addBindableUpdater(property); } } return all; }, //<debug> applyReference: function (reference) { var validIdRe = this.validRefRe || Ext.validIdRe; if (reference && !validIdRe.test(reference)) { Ext.raise('Invalid reference "' + reference + '" for ' + this.getId() + ' - not a valid identifier'); } return reference; }, //</debug> /** * Transforms a Session config to a proper instance. * @param {Object} session * @return {Ext.data.Session} * @private * @since 5.0.0 */ applySession: function (session) { if (!session) { return null; } if (!session.isSession) { var parentSession = this.lookupSession(true), // skip this component config = (session === true) ? {} : session; if (parentSession) { session = parentSession.spawn(config); } else { // Mask this use of Session from Cmd - the dependency is not ours but // the caller session = new Ext.data['Session'](config); } } return session; }, /** * Transforms a ViewModel config to a proper instance. * @param {String/Object/Ext.app.ViewModel} viewModel * @return {Ext.app.ViewModel} * @private * @since 5.0.0 */ applyViewModel: function (viewModel) { var me = this, config, session; if (!viewModel) { return null; } if (!viewModel.isViewModel) { config = { parent: me.lookupViewModel(true), // skip this component // Ensure that VM construction activity can reach the view (for // example events on stores) view: me }; config.session = me.getSession(); if (!session && !config.parent) { config.session = me.lookupSession(); } if (viewModel) { if (viewModel.constructor === Object) { Ext.apply(config, viewModel); } else if (typeof viewModel === 'string') { config.type = viewModel; } } viewModel = Ext.Factory.viewModel(config); } return viewModel; }, _getBindTemplateScope: function () { // This method is called as a method on a Binding instance, so the "this" pointer // is that of the Binding. The "scope" of the Binding is the component owning it. return this.scope.resolveListenerScope(); }, clearBindableUpdater: function (property) { var me = this, configs = me.self.$config.configs, cfg = configs[property], updateName; if (cfg && me.hasOwnProperty(updateName = cfg.names.update)) { if (me[updateName].$bindableUpdater) { delete me[updateName]; } } }, destroyBindable: function() { var me = this, viewModel = me.getConfig('viewModel', true), session = me.getConfig('session', true), controller = me.getController(); if (viewModel && viewModel.isViewModel) { viewModel.destroy(); me.setViewModel(null); } if (session && session.isSession) { if (session.getAutoDestroy()) { session.destroy(); } me.setSession(null); } if (controller) { me.setController(null); controller.destroy(); } }, /** * This method triggers the lazy configs and must be called when it is time to * fully boot up. The configs that must be initialized are: `bind`, `publishes`, * `session`, `twoWayBindable` and `viewModel`. * @private * @since 5.0.0 */ initBindable: function () { var me = this; me.initBindable = Ext.emptyFn; me.getBind(); me.getPublishes(); // If we have binds, the applyBind method will call getTwoWayBindable to ensure // we have the necessary updaters. If we have no binds then applyBind will not // be called and we will ignore our twoWayBindable config (which is fine). // // If we have publishes or binds then the viewModel will be requested. if (!me.viewModel) { // Force VM creation now me.getViewModel(); } }, /** * Returns an `update` method for the given Config that will call `{@link #publishState}` * to ensure two-way bindings (via `bind`) as well as any `publishes` are updated. * This method is cached on the `cfg` instance for re-use. * @param {Ext.Config} cfg * @return {Function} The updater function. * @private * @since 5.0.0 */ makeBindableUpdater: function (cfg) { var updateName = cfg.names.update, fn = function (newValue, oldValue) { var me = this, updater = me.self.prototype[updateName]; if (updater) { updater.call(me, newValue, oldValue); } me.publishState(cfg.name, newValue); }; fn.$bindableUpdater = true; return fn; }, /** * Checks if a particular binding is synchronizing the value. * @param {String} name The name of the property being bound to. * @return {Boolean} `true` if the binding is syncing. * * @protected */ isSyncing: function(name) { var bindings = this.getBind(), ret = false, binding; if (bindings) { binding = bindings[name]; if (binding) { ret = binding.syncing > 0; } } return ret; }, onBindNotify: function (value, oldValue, binding) { binding.syncing = (binding.syncing + 1) || 1; this[binding._config.names.set](value); --binding.syncing; }, removeBindings: function() { var me = this, bindings, key, binding; if (me.$hasBinds) { bindings = me.getBind(); if (bindings && typeof bindings !== 'string') { for (key in bindings) { binding = bindings[key]; binding.destroy(); binding._config = binding.getTemplateScope = null; } } } me.setBind(null); }, /** * Updates the session config. * @param {Ext.data.Session} session * @private */ updateSession: function (session) { var state = this.getInherited(); if (session) { state.session = session; } else { delete state.session; } }, /** * Updates the viewModel config. * @param {Ext.app.ViewModel} viewModel * @param {Ext.app.ViewModel} oldViewModel * @private */ updateViewModel: function (viewModel) { var me = this, state = me.getInherited(), controller = me.getController(); if (viewModel) { state.viewModel = viewModel; viewModel.setView(me); if (controller) { controller.initViewModel(viewModel); } } else { delete state.viewModel; } // In classic, this does nothing, in modern it will save a local // reference me.viewModel = viewModel; } } // private });