/** * This class manages arbitrary data and its relationship to data models. Instances of * `ViewModel` are associated with some `Component` and then used by their child items * for the purposes of Data Binding. * * # Binding * * The most commonly used aspect of a `ViewModel` is the `bind` method. This method takes * a "bind descriptor" (see below) and a callback to call when the data indicated by the * bind descriptor either becomes available or changes. * * The `bind` method, based on the bind descriptor given, will return different types of * "binding" objects. These objects maintain the connection between the requested data and * the callback. Bindings ultimately derive from `{@link Ext.app.bind.BaseBinding}` * which provides several methods to help manage the binding. * * Perhaps the most important method is `destroy`. When the binding is no longer needed * it is important to remember to `destroy` it. Leaking bindings can cause performance * problems or worse when callbacks are called at unexpected times. * * The types of bindings produced by `bind` are: * * * `{@link Ext.app.bind.Binding}` * * `{@link Ext.app.bind.Multi}` * * `{@link Ext.app.bind.TemplateBinding}` * * ## Bind Descriptors * * A "bind descriptor" is a value (a String, an Object or an array of these) that describe * the desired data. Any piece of data in the `ViewModel` can be described by a bind * descriptor. * * ### Textual Bind Descriptors * * The simplest and most common form of bind descriptors are strings that look like an * `Ext.Template` containing text and tokens surrounded by "{}" with dot notation inside * to traverse objects and their properties. * * For example: * * * `'Hello {user.name}!'` * * `'You have selected "{selectedItem.text}".'` * * `'{!isDisabled}'` * * `'{a > b ? "Bigger" : "Smaller"}'` * * `'{user.groups}'` * * All except the last are `{@link Ext.app.bind.TemplateBinding template bindings}` * which use the familiar `Ext.Template` syntax with some slight differences. For more on * templates see `{@link Ext.app.bind.Template}`. * * The last descriptor is called a "direct bind descriptor". This special form of * bind maps one-to-one to some piece of data in the `ViewModel` and is managed by the * `{@link Ext.app.bind.Binding}` class. * * #### Two-Way Descriptors * * A direct bind descriptor may be able to write back a value to the `ViewModel` as well * as retrieve one. When this is the case, they are said to be "two-way". For example: * * var binding = viewModel.bind('{s}', function(s) { console.log('s=' + s); }); * * binding.setValue('abc'); * * Direct use of `ViewModel` in this way is not commonly needed because `Ext.Component` * automates this process. For example, a `textfield` component understands when it is * given a "two-way" binding and automatically synchronizes its value bidirectionally using * the above technique. For example: * * Ext.widget({ * items: [{ * xtype: 'textfield', * bind: '{s}' // a two-way / direct bind descriptor * }] * }); * * ### Object and Array Descriptors / Multi-Bind * * With two exceptions (see below) an Object is interpreted as a "shape" to produce by * treating each of its properties as individual bind descriptors. An object of the same * shape is passed as the value of the bind except that each property is populated with * the appropriate value. Of course, this definition is recursive, so these properties * may also be objects. * * For example: * * viewModel.bind({ * x: '{x}', * foo: { * bar: 'Hello {foo.bar}' * } * }, * function (obj) { * // obj = { * // x: 42, * // foo: { * // bar: 'Hello foobar' * // } * // } * }); * * Arrays are handled in the same way. Each element of the array is considered a bind * descriptor (recursively) and the value produced for the binding is an array with each * element set to the bound property. * * ### Bind Options * * One exception to the "object is a multi-bind" rule is when that object contains a * `bindTo` property. When an object contains a `bindTo` property the object is understood * to contain bind options and the value of `bindTo` is considered the actual bind * descriptor. * * For example: * * viewModel.bind({ * bindTo: '{x}', * single: true * }, * function (x) { * console.log('x: ' + x); // only called once * }); * * The available bind options depend on the type of binding, but since all bindings * derive from `{@link Ext.app.bind.BaseBinding}` its options are always applicable. * For a list of the other types of bindings, see above. * * #### Deep Binding * * When a direct bind is made and the bound property is an object, by default the binding * callback is only called when that reference changes. This is the most efficient way to * understand a bind of this type, but sometimes you may need to be notified if any of the * properties of that object change. * * To do this, we create a "deep bind": * * viewModel.bind({ * bindTo: '{someObject}', * deep: true * }, * function (someObject) { * // called when reference changes or *any* property changes * }); * * #### Binding Timings * * The `ViewModel` has a {@link #scheduler} attached that is used to coordinate the firing of * bindings. * It serves 2 main purposes: * - To coordinate dependencies between bindings. This means bindings will be fired in an order * such that the any dependencies for a binding are fired before the binding itself. * - To batch binding firings. The scheduler runs on a short timer, so the following code will * only trigger a single binding (the last), the changes in between will never be triggered. * * Example: * * viewModel.bind('{val}', function(v) { * console.log(v); * }); * viewModel.set('val', 1); * viewModel.set('val', 2); * viewModel.set('val', 3); * viewModel.set('val', 4); * * The `ViewModel` can be forced to process by calling `{@link #notify}`, which will force the * scheduler to run immediately in the current state. * * viewModel.bind('{val}', function(v) { * console.log(v); * }); * viewModel.set('val', 1); * viewModel.notify(); * viewModel.set('val', 2); * viewModel.notify(); * viewModel.set('val', 3); * viewModel.notify(); * viewModel.set('val', 4); * viewModel.notify(); * * * #### Models, Stores and Associations * * A {@link Ext.data.Session Session} manages model instances and their associations. * The `ViewModel` may be used with or without a `Session`. When a `Session` is attached, the * `ViewModel` will always consult the `Session` to ask about records and stores. The `Session` * ensures that only a single instance of each model Type/Id combination is created. This is * important when tracking changes in models so that we always have the same reference. * * A `ViewModel` provides functionality to easily consume the built in data package types * {@link Ext.data.Model} and {@link Ext.data.Store}, as well as their associations. * * ### Model Links * * A model can be described declaratively using {@link #links}. In the example code below, * We ask the `ViewModel` to construct a record of type `User` with `id: 17`. The model will be * loaded from the server and the bindings will trigger once the load has completed. Similarly, * we could also attach a model instance to the `ViewModel` data directly. * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: ['name'] * }); * * var rec = new MyApp.model.User({ * id: 12, * name: 'Foo' * }); * * var viewModel = new Ext.app.ViewModel({ * links: { * theUser: { * type: 'User', * id: 17 * } * }, * data: { * otherUser: rec * } * }); * viewModel.bind('{theUser.name}', function(v) { * console.log(v); * }); * viewModel.bind('{otherUser.name}', function(v) { * console.log(v); * }); * * ### Model Fields * * Bindings have the functionality to inspect the parent values and resolve the underlying * value dynamically. This behavior allows model fields to be interrogated as part of a binding. * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: ['name', 'age'] * }); * * var viewModel = new Ext.app.ViewModel({ * links: { * theUser: { * type: 'User', * id: 22 * } * } * }); * * // Server responds with: * { * "id": 22, * "name": "Foo", * "age": 100 * } * * viewModel.bind('Hello {name}, you are {age} years old', function(v) { * console.log(v); * }); * * ### Record Properties * * It is possible to bind to the certain state properties of a record. The available options are: * - `{@link Ext.data.Model#property-dirty dirty}` * - `{@link Ext.data.Model#property-phantom phantom}` * - `{@link Ext.data.Model#method-isValid valid}` * * Example usage: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: [{ * name: 'name', * validators: 'presence' * }, { * name: 'age', * validators: { * type: 'range', * min: 0 * } * }] * }); * * var rec = new MyApp.model.User(); * * var viewModel = new Ext.app.ViewModel({ * data: { * theUser: rec * } * }); * * viewModel.bind({ * dirty: '{theUser.dirty}', * phantom: '{theUser.phantom}', * valid: '{theUser.valid}' * }, function(v) { * console.log(v.dirty, v.valid); * }); * * rec.set('name', 'Foo'); * viewModel.notify(); // dirty, not valid * rec.set('age', 20); * viewModel.notify(); // dirty, valid * rec.reject(); * viewModel.notify(); // not dirty, not valid * * ### Advanced Record Binding * * For accessing other record information that is not exposed by the binding API, formulas * can be used to achieve more advanced operations: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: ['name', 'age'] * }); * * var rec = new MyApp.model.User(); * * var viewModel = new Ext.app.ViewModel({ * formulas: { * isNameModified: { * bind: { * bindTo: '{theUser}', * deep: true * }, * get: function(rec) { * return rec.isModified('name'); * } * } * }, * data: { * theUser: rec * } * }); * * viewModel.bind('{isNameModified}', function(modified) { * console.log(modified); * }); * rec.set('name', 'Foo'); * * ### Associations * * In the same way as fields, the bindings can also traverse associations in a bind statement. * The `ViewModel` will handle the asynchronous loading of data and only present the value once * the full path has been loaded. For more information on associations see * {@link Ext.data.schema.OneToOne OneToOne} and {@link Ext.data.schema.ManyToOne ManyToOne} * associations. * * Ext.define('User', { * extend: 'Ext.data.Model', * fields: ['name'] * }); * * Ext.define('Order', { * extend: 'Ext.data.Model', * fields: ['date', { * name: 'userId', * reference: 'User' * }] * }); * * Ext.define('OrderItem', { * extend: 'Ext.data.Model', * fields: ['price', 'qty', { * name: 'orderId', * reference: 'Order' * }] * }); * * var viewModel = new Ext.app.ViewModel({ * links: { * orderItem: { * type: 'OrderItem', * id: 13 * } * } * }); * // The viewmodel will handle both ways of loading the data: * // a) If the data is loaded inline in a nested fashion it will * // not make requests for extra data * // b) Only loading a single model at a time. So the Order will be loaded once * // the OrderItem returns. The User will be loaded once the Order loads. * viewModel.bind('{orderItem.order.user.name}', function(name) { * console.log(name); * }); * * ### Stores * * Stores can be created as part of the `ViewModel` definition. The definitions are processed * like bindings which allows for very powerful dynamic functionality. * * It is important to ensure that you name viewModel's data keys uniquely. If data is not named * uniquely, binds and formulas may receive information from an unintended data source. * This applies to keys in the viewModel's data block, stores, and links configs. * * var viewModel = new Ext.app.ViewModel({ * stores: { * users: { * model: 'User', * autoLoad: true, * filters: [{ * property: 'createdDate', * value: '{createdFilter}', * operator: '>' * }] * } * } * }); * // Later on in our code, we set the date so that the store is created. * viewModel.set('createdFilter', Ext.Date.subtract(new Date(), Ext.Date.DAY, 7)); * * See {@link #stores} for more detail. * * ### Store Properties * * It is possible to bind to the certain state properties of the store. The available options are: * - `{@link Ext.data.Store#method-getCount count}` * - `{@link Ext.data.Store#method-first}` * - `{@link Ext.data.Store#method-last}` * - `{@link Ext.data.Store#method-hasPendingLoad loading}` * - `{@link Ext.data.Store#method-getTotalCount totalCount}` * * Example: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: ['name'] * }); * * var viewModel = new Ext.app.ViewModel({ * stores: { * users: { * model: 'MyApp.model.User', * data: [{ * name: 'Foo' * }, { * name: 'Bar' * }] * } * } * }); * * viewModel.bind('{users.first}', function(first) { * console.log(first ? first.get('name') : 'Nobody'); * }); * * var timer = Ext.interval(function() { * var store = viewModel.getStore('users'); * if (store.getCount()) { * store.removeAt(0); * } else { * Ext.uninterval(timer); * } * }, 100); * * ### Advanced Store Binding * * For accessing other store information that is not exposed by the binding API, formulas * can be used to achieve more advanced operations: * * Ext.define('MyApp.model.User', { * extend: 'Ext.data.Model', * fields: ['name', 'score'] * }); * * var viewModel = new Ext.app.ViewModel({ * stores: { * users: { * model: 'MyApp.model.User', * data: [{ * name: 'Foo', * score: 100 * }, { * name: 'Bar', * score: 350 * }] * } * }, * formulas: { * totalScore: { * bind: { * bindTo: '{users}', * deep: true * }, * get: function(store) { * return store.sum('score'); * } * } * } * }); * * viewModel.bind('{totalScore}', function(score) { * console.log(score); * }); * * viewModel.notify(); * viewModel.getStore('users').removeAll(); * * #### Formulas * * Formulas allow for calculated `ViewModel` data values. The dependencies for these formulas * are automatically determined so that the formula will not be processed until the required * data is present. * * var viewModel = new Ext.app.ViewModel({ * formulas: { * fullName: function(get) { * return get('firstName') + ' ' + get('lastName'); * } * }, * data: {firstName: 'John', lastName: 'Smith'} * }); * * viewModel.bind('{fullName}', function(v) { * console.log(v); * }); * * See {@link #formulas} for more detail. * * #### Inheriting Data With Nesting * * ViewModels can have a {@link #parent} which allows values to be consumed from * a shared base. These values that are available from the {@link #parent} are not copied, * rather they are "inherited" in a similar fashion to a javascript closure scope chain. * This is demonstrated in the example below: * * var parent = new Ext.app.ViewModel({ * data: { * foo: 3 * } * }); * var child = new Ext.app.ViewModel({ * parent: parent * }); * * This is analogous to the following javascript closure: * * var foo = 3; * Ext.Ajax.request({ * success: function() { * // foo is available here * } * }); * * ### Climbing/Inheriting * * In line with the above, the default behaviour when setting the value of a child ViewModel * (either) through {@link #set} or {@link Ext.app.bind.Binding#method-setValue} is to climb to * where the value is "owned" and set the value there: * * var parent = new Ext.app.ViewModel({ * data: { * foo: 3 * } * }); * var child = new Ext.app.ViewModel({ * parent: parent * }); * * child.set('foo', 100); // Climbs to set the value on parent * console.log(parent.get('foo')); // 100 * parent.set('foo', 200); * console.log(child.get('foo')); // 200, inherited from the parent * * Any subsequent sets are also inherited in the same fashion. The inheriting/climbing behavior * occurs for any arbitrary depth, climbing/inherting can owned by a parent at any level above. * * function log() { * console.log([a, b, c, d, e].map(function(vm) { * return vm.get('foo'); * })); * } * * var a = new Ext.app.ViewModel({data: {foo: 3}}), * b = new Ext.app.ViewModel({parent: a}), * c = new Ext.app.ViewModel({parent: b}), * d = new Ext.app.ViewModel({parent: c}), * e = new Ext.app.ViewModel({parent: d}); * * log(); // [3, 3, 3, 3, 3] * * e.set('foo', 100); * log(); // [100, 100, 100, 100, 100] * * This same climbing behavior applies when setting a value on a binding. The climbing begins from * the ViewModel where the binding was attached: * * function log() { * console.log([a, b, c].map(function(vm) { * return vm.get('foo'); * })); * } * * var a = new Ext.app.ViewModel({data: {foo: 3}}), * b = new Ext.app.ViewModel({parent: a}), * c = new Ext.app.ViewModel({parent: b}); * * var bind = c.bind('{foo}', function() {}); * * bind.setValue(100); * log(); // [100, 100, 100] * * The exception to this rule is when there is nothing above to climb to. If a value is set and * there is no parent above to hold it, then the value is set where it was called: * * function log() { * console.log([a, b, c].map(function(vm) { * return vm.get('foo'); * })); * } * * var a = new Ext.app.ViewModel(), * b = new Ext.app.ViewModel({parent: a}), * c = new Ext.app.ViewModel({parent: b}); * * c.set('foo', 3); * log(); // [null, null, 3] * * b.set('foo', 2); * log(); // [null, 2, 3] * * a.set('foo', 1); * log(); // [1, 2, 3] * * These values are called local values, which are discussed below. * * ### Local Values * * If the child ViewModel is declared with top level data that also exists in the parent, then that * child is considered to own that local value, so no value is inherited from the parent, nor does * the climbing behaviour occur. * * var parent = new Ext.app.ViewModel({ * data: { * foo: 3 * } * }); * var child = new Ext.app.ViewModel({ * parent: parent, * data: { * foo: 5 * } * }); * * console.log(parent.get('foo'), child.get('foo')); // 3, 5 * child.set('foo', 100); * console.log(parent.get('foo'), child.get('foo')); // 3, 100 * parent.set('foo', 200); * console.log(parent.get('foo'), child.get('foo')); // 200, 100 * * The inheriting/climbing behavior is limited to local values: * * function log() { * console.log([a, b, c, d, e].map(function(vm) { * return vm.get('foo'); * })); * } * * var a = new Ext.app.ViewModel({data: {foo: 1}}), * b = new Ext.app.ViewModel({parent: a}), * c = new Ext.app.ViewModel({parent: b, data: {foo: 2}}), * d = new Ext.app.ViewModel({parent: c}), * e = new Ext.app.ViewModel({parent: d, data: {foo: 3}}); * * log(); // [1, 1, 2, 2, 3] * * e.set('foo', 100); * log(); // [1, 1, 2, 2, 100] * * d.set('foo', 200); * log(); // [1, 1, 200, 200, 100] * * c.set('foo', 201); * log(); // [1, 1, 201, 201, 100] * * b.set('foo', 300); * log(); // [300, 300, 201, 201, 100] * * a.set('foo', 301); * log(); // [301, 301, 201, 201, 100] * * ### Attaching/Clearing Local Values Dynamically * * To bypass the climbing behaviour and push a value into a particular point * in the hierarchy, the {@link #setData} method should be used. Once a local value * is set, it will be used as such in the future. * * function log() { * console.log([a, b, c, d, e].map(function(vm) { * return vm.get('foo'); * })); * } * * var a = new Ext.app.ViewModel({data: {foo: 3}}), * b = new Ext.app.ViewModel({parent: a}), * c = new Ext.app.ViewModel({parent: b}), * d = new Ext.app.ViewModel({parent: c}), * e = new Ext.app.ViewModel({parent: d}); * * log(); // [3, 3, 3, 3, 3] * * c.setData({ * foo: 100 * }); * * log(); // [3, 3, 100, 100, 100] * * d.set('foo', 200); // Climbs to new local value * log(); // [3, 3, 200, 200, 200] * * Similarly, data can be cleared from being a local value by setting the value to undefined: * * function log() { * console.log([a, b, c, d].map(function(vm) { * return vm.get('foo'); * })); * } * * var a = new Ext.app.ViewModel({data: {foo: 3}}), * b = new Ext.app.ViewModel({parent: a}), * c = new Ext.app.ViewModel({parent: b, data: {foo: 100}}), * d = new Ext.app.ViewModel({parent: c}); * * log(); // [3, 3, 100, 100] * * c.setData({ * foo: undefined * }); * log([3, 3, 3, 3]); * */Ext.define('Ext.app.ViewModel', { mixins: [ 'Ext.mixin.Factoryable', 'Ext.mixin.Identifiable' ], requires: [ 'Ext.util.Scheduler', 'Ext.data.Session', 'Ext.app.bind.RootStub', 'Ext.app.bind.LinkStub', 'Ext.app.bind.Multi', 'Ext.app.bind.Formula', 'Ext.app.bind.TemplateBinding', // TODO: this is an injected dependency in onStoreBind, need to define so // cmd can detect it 'Ext.data.ChainedStore' ], alias: 'viewmodel.default', // also configures Factoryable isViewModel: true, factoryConfig: { name: 'viewModel' }, collectTimeout: 100, expressionRe: /^(?:\{(?:(\d+)|([a-z_][\w.]*))\})$/i, statics: { /** * Escape bind strings so they are treated as literals. * * @param {Object/String} value The value to escape. If the value is * an object, any strings will be recursively escaped. * @return {Object/String} The escaped value. Matches the type of the * passed value. * * @since 6.5.2 * @private */ escape: function(value) { var ret = value, key; if (typeof value === 'string') { ret = '~~' + value; } else if (value && value.constructor === Object) { ret = {}; for (key in value) { ret[key] = this.escape(value[key]); } } return ret; } }, $configStrict: false, // allow "formulas" to be specified on derived class body config: { /** * @cfg {Object} data * This object holds the arbitrary data that populates the `ViewModel` and is * then available for binding. * @since 5.0.0 */ data: true, /** * @cfg {Object} formulas * An object that defines named values whose value is managed by function calls. * The names of the properties of this object are assigned as values in the * ViewModel. * * For example: * * formulas: { * xy: function (get) { return get('x') * get('y'); } * } * * For more details about defining a formula, see `{@link Ext.app.bind.Formula}`. * @since 5.0.0 */ formulas: { $value: null, merge: function(newValue, currentValue, target, mixinClass) { return this.mergeNew(newValue, currentValue, target, mixinClass); } }, /** * @cfg {Object} links * Links provide a way to assign a simple name to a more complex bind. The primary * use for this is to assign names to records in the data model. * * links: { * theUser: { * type: 'User', * id: 12 * } * } * * It is also possible to force a new phantom record to be created by not specifying an * id but passing `create: true` as part of the descriptor. This is often useful when * creating a new record for a child session. * * links: { * newUser: { * type: 'User', * create: true * } * } * * `create` can also be an object containing initial data for the record. * * links: { * newUser: { * type: 'User', * create: { * firstName: 'John', * lastName: 'Smith' * } * } * } * * While that is the typical use, the value of each property in `links` may also be * a bind descriptor (see `{@link #method-bind}` for the various forms of bind * descriptors). * @since 5.0.0 */ links: null, /** * @cfg {Ext.app.ViewModel} parent * The parent `ViewModel` of this `ViewModel`. Once set, this cannot be changed. * @readonly * @since 5.0.0 */ parent: null, /** * @cfg {Ext.app.bind.RootStub} root * A reference to the root "stub" (an object that manages bindings). * @private * @since 5.0.0 */ root: true, /** * @cfg {Ext.util.Scheduler} scheduler * The scheduler used to schedule and manage the delivery of notifications for * all connections to this `ViewModel` and any other attached to it. The normal * process to initialize the `scheduler` is to get the scheduler used by the * `parent` or `session` and failing either of those, create one. * @readonly * @private * @since 5.0.0 */ scheduler: null, /** * @cfg {String/Ext.data.schema.Schema} schema * The schema to use for getting information about entities. */ schema: 'default', /** * @cfg {Ext.data.Session} session * The session used to manage the data model (records and stores). * @since 5.0.0 */ session: null, // @cmd-auto-dependency {isKeyedObject: true, aliasPrefix: "store.", defaultType: "store"} /** * @cfg {Object} stores * A declaration of `Ext.data.Store` configurations that are first processed as * binds to produce an effective store configuration. * * A simple store definition. We can reference this in our bind statements using the * `{users}` as we would with other data values. * * new Ext.app.ViewModel({ * stores: { * users: { * model: 'User', * autoLoad: true * } * } * }); * * This store definition contains a dynamic binding. The store will not be created until * the initial value for groupId is set. Once that occurs, the store is created with the * appropriate filter configuration. Subsequently, once we change the group value, the old * filter will be overwritten with the new value. * * var viewModel = new Ext.app.ViewModel({ * stores: { * users: { * model: 'User', * filters: [{ * property: 'groupId', * value: '{groupId}' * }] * } * } * }); * viewModel.set('groupId', 1); // This will trigger the store creation with the filter. * viewModel.set('groupId', 2); // The filter value will be changed. * * This store uses {@link Ext.data.ChainedStore store chaining} to create a store backed by * the data in another store. By specifying a string as the store, it will bind our creation * and backing to the other store. This functionality is especially useful when wanting to * display a different "view" of a store, for example a different sort order or different * filters. * * var viewModel = new Ext.app.ViewModel({ * stores: { * allUsers: { * model: 'User', * autoLoad: true * }, * children: { * source: '{allUsers}', * filters: [{ * property: 'age', * value: 18, * operator: '<' * }] * } * } * }); * * @since 5.0.0 */ stores: null, /** * @cfg {Ext.container.Container} view * The Container that owns this `ViewModel` instance. * @since 5.0.0 */ view: null }, constructor: function(config) { // Used to track non-stub bindings this.bindings = {}; /* * me.data = { * foo: { * }, * * selectedUser: { * name: null * }, * } * * me.root = new Ext.app.bind.RootStub({ * children: { * foo: new Ext.app.bind.Stub(), * selectedUser: new Ext.app.bind.LinkStub({ * binding: session.bind(...), * children: { * name: : new Ext.app.bind.Stub() * } * }), * } * }) */ this.initConfig(config); }, destroy: function() { var me = this, scheduler = me._scheduler, stores = me.storeInfo, parent = me.getParent(), task = me.collectTask, children = me.children, bindings = me.bindings, key, store, autoDestroy, storeBinding; me.destroying = true; if (task) { task.cancel(); me.collectTask = null; } // When used with components, they are destroyed bottom up // so this scenario is only likely to happen in the case where // we're using the VM without any component attachment, in which case // we need to clean up here. if (children) { for (key in children) { children[key].destroy(); } } if (stores) { for (key in stores) { store = stores[key]; // Cache this property in case store is destroyed; // Properties are cleared on destroy storeBinding = store.$binding; autoDestroy = store.autoDestroy; if (autoDestroy || (!store.$wasInstance && autoDestroy !== false)) { store.destroy(); } Ext.destroy(storeBinding); } } if (parent) { parent.unregisterChild(me); } me.getRoot().destroy(); for (key in bindings) { bindings[key].destroy(); } if (scheduler && scheduler.$owner === me) { scheduler.$owner = null; scheduler.destroy(); } me.children = me.storeInfo = me._session = me._view = me._scheduler = me.bindings = me._root = me._parent = me.formulaFn = me.$formulaData = null; // This just makes it hard to ask "was destroy() called?": // me.destroying = false; // removed in 7.0 me.callParent(); }, /** * This method requests that data in this `ViewModel` be delivered to the specified * `callback`. The data desired is given in a "bind descriptor" which is the first * argument. * * A simple call might look like this: * * var binding = vm.bind('{foo}', this.onFoo, this); * * binding.destroy(); // when done with the binding * * Options for the binding can be provided in the last argument: * * var binding = vm.bind('{foo}', this.onFoo, this, { * deep: true * }); * * Alternatively, bind options can be combined with the bind descriptor using only * the first argument: * * var binding = vm.bind({ * bindTo: '{foo}', // the presence of bindTo identifies this form * deep: true * }, this.onFoo, this); * * See the class documentation for more details on Bind Descriptors and options. * * @param {String/Object/Array} descriptor The bind descriptor. See class description * for details. * @param {Function} callback The function to call with the value of the bound property. * @param {Object} [scope] The scope (`this` pointer) for the `callback`. * @param {Object} [options] Additional options to configure the * {@link Ext.app.bind.Binding binding}. If this parameter is provided, the `bindTo` form * of combining options and bind descriptor is not recognized. * @return {Ext.app.bind.BaseBinding/Ext.app.bind.Binding} The binding. */ bind: function(descriptor, callback, scope, options) { var me = this, track = true, binding; scope = scope || me; if (!options && descriptor.bindTo !== undefined && !Ext.isString(descriptor)) { options = descriptor; descriptor = options.bindTo; } if (!Ext.isString(descriptor)) { binding = new Ext.app.bind.Multi(descriptor, me, callback, scope, options); } else if (me.expressionRe.test(descriptor)) { // If we have '{foo}' alone it is a literal descriptor = descriptor.substring(1, descriptor.length - 1); binding = me.bindExpression(descriptor, callback, scope, options); track = false; } else { binding = new Ext.app.bind.TemplateBinding(descriptor, me, callback, scope, options); } if (track) { me.bindings[binding.id] = binding; } return binding; }, /** * Gets the session attached to this (or a parent) ViewModel. See the {@link #session} * configuration. * @return {Ext.data.Session} The session. `null` if no session exists. */ getSession: function() { var me = this, session = me._session, parent; if (!session && (parent = me.getParent())) { me.setSession(session = parent.getSession()); } return session || null; }, /** * Gets a store configured via the {@link #stores} configuration. * @param {String} key The name of the store. * @return {Ext.data.Store} The store. `null` if no store exists. */ getStore: function(key) { var storeInfo = this.storeInfo, store; if (storeInfo) { store = storeInfo[key]; } return store || null; }, /** * @method getStores * @hide */ /** * Create a link to a reference. See the {@link #links} configuration. * @param {String} key The name for the link. * @param {Object} reference The reference descriptor. */ linkTo: function(key, reference) { var me = this, stub, create, id, modelType, linkStub, rec; //<debug> if (key.indexOf('.') > -1) { Ext.raise('Links can only be at the top-level: "' + key + '"'); } //</debug> if (reference.isModel) { reference = { type: reference.entityName, id: reference.id }; } // reference is backwards compat, type is preferred. modelType = reference.type || reference.reference; create = reference.create; if (modelType) { // It's a record id = reference.id; //<debug> if (!reference.create && Ext.isEmpty(id)) { Ext.raise('No id specified. To create a phantom model, specify "create: true" ' + 'as part of the reference.'); } //</debug> if (create) { id = undefined; } rec = me.getRecord(modelType, id); if (Ext.isObject(create)) { rec.set(create); rec.commit(); rec.phantom = true; } // Force creation at the root level. If an existing stub is there // it will be grafted in place here. stub = me.getRoot().createStubChild(key); stub.set(rec); } else { stub = me.getStub(key); if (!stub.isLinkStub) { // Pass parent=null since we will graft in this new stub to replace us: linkStub = new Ext.app.bind.LinkStub(me, stub.name); stub.graft(linkStub); stub = linkStub; } stub.link(reference); } }, /** * Forces all bindings in this ViewModel hierarchy to evaluate immediately. Use this to do * a synchronous flush of all bindings. */ notify: function() { var scheduler = this.getScheduler(); if (!scheduler.firing) { scheduler.notify(); } }, /** * Get a value from the data for this viewmodel. * @param {String} path The path of the data to retrieve. * * var value = vm.get('theUser.address.city'); * * @return {Object} The data stored at the passed path. */ get: function(path) { return this.getStub(path).getValue(); }, /** * Set a value in the data for this viewmodel. This method will climb to set data on * a parent view model if appropriate. See "Inheriting Data" in the class introduction for * more information. * * @param {Object/String} path The path of the value to set, or an object literal to set * at the root of the viewmodel. * @param {Object} value The data to set at the value. If the value is an object literal, * any required paths will be created. * * // Set a single property at the root level * viewModel.set('expiry', Ext.Date.add(new Date(), Ext.Date.DAY, 7)); * console.log(viewModel.get('expiry')); * // Sets a single property in user.address, does not overwrite any hierarchy. * viewModel.set('user.address.city', 'London'); * console.log(viewModel.get('user.address.city')); * // Sets 2 properties of "user". Overwrites any existing hierarchy. * viewModel.set('user', {firstName: 'Foo', lastName: 'Bar'}); * console.log(viewModel.get('user.firstName')); * // Sets a single property at the root level. Overwrites any existing hierarchy. * viewModel.set({rootKey: 1}); * console.log(viewModel.get('rootKey')); */ set: function(path, value) { var me = this, obj, stub; // Force data creation me.getData(); if (value === undefined && path && path.constructor === Object) { stub = me.getRoot(); value = path; } else if (path && path.indexOf('.') < 0) { obj = {}; obj[path] = value; value = obj; stub = me.getRoot(); } else { stub = me.getStub(path); } stub.set(value); }, /** * Sets data directly at the level of this viewmodel. This method does not climb * to set data on parent view models. Passing `undefined` will clear the value * in this viewmodel, which means that this viewmodel is free to inherit data * from a parent. See "Inheriting Data" in the class introduction for more information. * @param {Object} data The new data to set. * @method setData */ //========================================================================= privates: { registerChild: function(child) { var children = this.children; if (!children) { this.children = children = {}; } children[child.getId()] = child; }, unregisterChild: function(child) { var children = this.children; // If we're destroying we'll be wiping this collection shortly, so // just ignore it here if (!this.destroying && children) { delete children[child.getId()]; } }, /** * Get a record instance given a reference descriptor. Will ask * the session if one exists. * @param {String/Ext.Class} type The model type. * @param {Object} id The model id. * @return {Ext.data.Model} The model instance. * @private */ getRecord: function(type, id) { var session = this.getSession(), Model = type, hasId = id !== undefined, record; if (session) { if (hasId) { record = session.getRecord(type, id); } else { record = session.createRecord(type); } } else { if (!Model.$isClass) { Model = this.getSchema().getEntity(Model); //<debug> if (!Model) { Ext.raise('Invalid model name: ' + type); } //</debug> } if (hasId) { record = Model.createWithId(id); record.load(); } else { record = new Model(); } } return record; }, bindExpression: function(descriptor, callback, scope, options) { var stub = this.getStub(descriptor); return stub.bind(callback, scope, options); }, applyScheduler: function(scheduler) { if (scheduler && !scheduler.isInstance) { if (scheduler === true) { scheduler = {}; } if (!('preSort' in scheduler)) { scheduler = Ext.apply({ preSort: 'kind,-depth' }, scheduler); } scheduler = new Ext.util.Scheduler(scheduler); scheduler.$owner = this; } return scheduler; }, getScheduler: function() { var me = this, scheduler = me._scheduler, parent; if (!scheduler) { if (!(parent = me.getParent())) { scheduler = new Ext.util.Scheduler({ // See Session#scheduler preSort: 'kind,-depth' }); scheduler.$owner = me; } else { scheduler = parent.getScheduler(); } me.setScheduler(scheduler); } return scheduler; }, /** * This method looks up the `Stub` for a single bind descriptor. * @param {String/Object} bindDescr The bind descriptor. * @return {Ext.app.bind.AbstractStub} The `Stub` associated to the bind descriptor. * @private */ getStub: function(bindDescr) { var root = this.getRoot(); return bindDescr ? root.getChild(bindDescr) : root; }, collect: function() { var me = this, parent = me.getParent(), task = me.collectTask; if (parent) { parent.collect(); return; } if (!task) { task = me.collectTask = new Ext.util.DelayedTask(me.doCollect, me); } // Useful for testing if (me.collectTimeout === 0) { me.doCollect(); } else { task.delay(me.collectTimeout); } }, doCollect: function() { var children = this.children, key; // We need to loop over the children first, since they may have link stubs // that create bindings inside our VM. Attempt to clean them up first. if (children) { for (key in children) { children[key].doCollect(); } } this.getRoot().collect(); }, invalidateChildLinks: function(name, clear) { var children = this.children, key; if (children) { for (key in children) { children[key].getRoot().invalidateChildLink(name, clear); } } }, onBindDestroy: function(binding, fromChild) { var me = this, parent; if (me.destroying) { return; } if (!fromChild) { delete me.bindings[binding.id]; } parent = me.getParent(); if (parent) { parent.onBindDestroy(binding, true); } else { me.collect(); } }, //------------------------------------------------------------------------- // Config // <editor-fold> applyData: function(newData, data) { var me = this, linkData, parent; // Force any session to be invoked so we can access it me.getSession(); if (!data) { parent = me.getParent(); /** * @property {Object} linkData * This object is used to hold the result of a linked value. This is done * so that the data object hasOwnProperty equates to whether or not this * property is owned by this instance or inherited. * @private * @readonly * @since 5.0.0 */ me.linkData = linkData = parent ? Ext.Object.chain(parent.getData()) : {}; /** * @property {Object} data * This object holds all of the properties of this `ViewModel`. It is * prototype chained to the `linkData` which is, in turn, prototype chained * to (if present) the `data` object of the parent `ViewModel`. * @private * @readonly * @since 5.0.0 */ me.data = me._data = Ext.Object.chain(linkData); } if (newData && newData.constructor === Object) { me.getRoot().set(newData, true); } }, applyParent: function(parent) { if (parent) { parent.registerChild(this); } return parent; }, applyStores: function(stores) { var me = this, root = me.getRoot(), key, cfg, storeBind, stub, listeners; me.storeInfo = {}; me.listenerScopeFn = function() { return me.getView().getInheritedConfig('defaultListenerScope'); }; for (key in stores) { cfg = stores[key]; if (cfg.isStore) { cfg.$wasInstance = true; me.setupStore(cfg, key); continue; } else if (Ext.isString(cfg)) { cfg = { source: cfg }; } else { cfg = Ext.apply({}, cfg); } // Get rid of listeners so they don't get considered as a bind listeners = cfg.listeners; delete cfg.listeners; storeBind = me.bind(cfg, me.onStoreBind, me, { trackStatics: true }); if (storeBind.isStatic()) { // Everything is static, we don't need to wait, so remove the // binding because it will only fire the first time. storeBind.destroy(); me.createStore(key, cfg, listeners); } else { storeBind.$storeKey = key; storeBind.$listeners = listeners; stub = root.createStubChild(key); stub.setStore(storeBind); } } }, onStoreBind: function(cfg, oldValue, binding) { var info = this.storeInfo, key = binding.$storeKey, store = info[key], proxy; if (!store) { this.createStore(key, cfg, binding.$listeners, binding); } else { cfg = Ext.merge({}, binding.pruneStaticKeys()); proxy = cfg.proxy; delete cfg.type; delete cfg.model; delete cfg.fields; delete cfg.proxy; delete cfg.listeners; // TODO: possibly optimize this so we can figure out what has changed // instead of smashing the whole lot if (proxy) { delete proxy.reader; delete proxy.writer; store.getProxy().setConfig(proxy); } store.setConfig(cfg); } }, createStore: function(key, cfg, listeners, binding) { var session = this.getSession(), store; cfg = Ext.apply({}, cfg); if (cfg.session) { cfg.session = session; } if (cfg.source) { cfg.type = cfg.type || 'chained'; } // Restore the listeners from applyStores here cfg.listeners = listeners; // Ensure events fired by ctor can find their target: cfg.resolveListenerScope = this.listenerScopeFn; store = Ext.Factory.store(cfg); store.$binding = binding; this.setupStore(store, key); }, setupStore: function(store, key) { var me = this, obj = {}; // Force data object creation me.getData(); // May have been given a store instance store.resolveListenerScope = me.listenerScopeFn; me.storeInfo[key] = store; obj[key] = store; me.setData(obj); }, applyFormulas: function(formulas) { var me = this, root = me.getRoot(), name, stub; me.getData(); // make sure our data is setup first for (name in formulas) { //<debug> if (name.indexOf('.') >= 0) { Ext.raise('Formula names cannot contain dots: ' + name); } //</debug> // Force a stub to be created root.createStubChild(name); stub = me.getStub(name); stub.setFormula(formulas[name]); } return formulas; }, applyLinks: function(links) { var link; for (link in links) { this.linkTo(link, links[link]); } return link; }, applySchema: function(schema) { return Ext.data.schema.Schema.get(schema); }, applyRoot: function() { var root = new Ext.app.bind.RootStub(this), parent = this.getParent(); if (parent) { // We are assigning the root of a child VM such that its bindings will be // pre-sorted after the bindings of the parent VM. root.depth = parent.getRoot().depth - 1000; } return root; }, getFormulaFn: function(data) { var me = this, fn = me.formulaFn; if (!fn) { fn = me.formulaFn = function(name) { // Note that the `this` pointer here is the view model because // the VM calls it in the VM scope. return me.$formulaData[name]; }; } me.$formulaData = data; return fn; } // </editor-fold> }});