messengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscord
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1210 lines
40 KiB
1210 lines
40 KiB
/** |
|
* 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}".'` |
|
* * `'{user.groups}'` |
|
* |
|
* The first two bindings 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 third bind 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(x) { 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. |
|
* |
|
* 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 a {@link #links `link`}. 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); |
|
* }); |
|
* |
|
* ### 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. |
|
* |
|
* #### 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. |
|
*/ |
|
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' |
|
}, |
|
|
|
destroyed: false, |
|
|
|
collectTimeout: 100, |
|
|
|
expressionRe: /^(?:\{[!]?(?:(\d+)|([a-z_][\w\-\.]*))\})$/i, |
|
|
|
$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) { |
|
this.hadValue = {}; |
|
/* |
|
* 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, |
|
key, store, autoDestroy; |
|
|
|
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]; |
|
autoDestroy = store.autoDestroy; |
|
if (autoDestroy || (!store.$wasInstance && autoDestroy !== false)) { |
|
store.destroy(); |
|
} |
|
Ext.destroy(store.$binding); |
|
} |
|
} |
|
|
|
if (parent) { |
|
parent.unregisterChild(me); |
|
} |
|
|
|
|
|
me.getRoot().destroy(); |
|
|
|
if (scheduler && scheduler.$owner === me) { |
|
scheduler.$owner = null; |
|
scheduler.destroy(); |
|
} |
|
|
|
me.hadValue = me.children = me.storeInfo = me._session = me._view = me._scheduler = |
|
me._root = me._parent = me.formulaFn = me.$formulaData = null; |
|
|
|
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. |
|
* |
|
* @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] |
|
* @return {Ext.app.bind.BaseBinding/Ext.app.bind.Binding} The binding. |
|
*/ |
|
bind: function (descriptor, callback, scope, options) { |
|
var me = this, |
|
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); |
|
} |
|
else { |
|
binding = new Ext.app.bind.TemplateBinding(descriptor, me, callback, scope, options); |
|
} |
|
|
|
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 = me.getStub(key), |
|
create, id, modelType, linkStub, rec; |
|
|
|
//<debug> |
|
if (stub.depth - me.getRoot().depth > 1) { |
|
Ext.Error.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.Error.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; |
|
} |
|
stub.set(rec); |
|
} else { |
|
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 () { |
|
this.getScheduler().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. |
|
* @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); |
|
}, |
|
|
|
//========================================================================= |
|
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.Error.raise('Invalid model name: ' + type); |
|
} |
|
//</debug> |
|
} |
|
if (hasId) { |
|
record = Model.createWithId(id); |
|
record.load(); |
|
} else { |
|
record = new Model(); |
|
} |
|
} |
|
return record; |
|
}, |
|
|
|
notFn: function (v) { |
|
return !v; |
|
}, |
|
|
|
bindExpression: function (descriptor, callback, scope, options) { |
|
var ch = descriptor.charAt(0), |
|
not = (ch === '!'), |
|
path = not ? descriptor.substring(1) : descriptor, |
|
stub = this.getStub(path), |
|
binding; |
|
|
|
binding = stub.bind(callback, scope, options); |
|
if (not) { |
|
binding.transform = this.notFn; |
|
} |
|
|
|
return binding; |
|
}, |
|
|
|
applyScheduler: function (scheduler) { |
|
if (scheduler && !scheduler.isInstance) { |
|
scheduler = new Ext.util.Scheduler(scheduler); |
|
scheduler.$owner = this; |
|
} |
|
return scheduler; |
|
}, |
|
|
|
getScheduler: function () { |
|
var me = this, |
|
scheduler = me._scheduler, |
|
parent, |
|
session; |
|
|
|
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(); |
|
}, |
|
|
|
onBindDestroy: function() { |
|
var me = this, |
|
parent; |
|
|
|
if (me.destroying) { |
|
return; |
|
} |
|
|
|
parent = me.getParent(); |
|
if (parent) { |
|
parent.onBindDestroy(); |
|
} 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); |
|
} |
|
}, |
|
|
|
applyParent: function(parent) { |
|
if (parent) { |
|
parent.registerChild(this); |
|
} |
|
return parent; |
|
}, |
|
|
|
applyStores: function(stores) { |
|
var me = this, |
|
root = me.getRoot(), |
|
key, cfg, storeBind, stub, listeners, isStatic; |
|
|
|
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.blockLoad(); |
|
store.setConfig(cfg); |
|
store.unblockLoad(true); |
|
} |
|
}, |
|
|
|
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; |
|
store = Ext.Factory.store(cfg); |
|
store.$binding = binding; |
|
this.setupStore(store, key); |
|
}, |
|
|
|
setupStore: function(store, key) { |
|
store.resolveListenerScope = this.listenerScopeFn; |
|
this.storeInfo[key] = store; |
|
this.set(key, store); |
|
}, |
|
|
|
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.Error.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) { |
|
for (var link in links) { |
|
this.linkTo(link, links[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> |
|
} |
|
});
|
|
|