skypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmail
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.
1211 lines
40 KiB
1211 lines
40 KiB
9 years ago
|
/**
|
||
|
* 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>
|
||
|
}
|
||
|
});
|