/** * This class and its derived classes are used to manage access to the properties of an * object stored in a `Session`. * @private */Ext.define('Ext.app.bind.Stub', { extend: 'Ext.app.bind.AbstractStub', requires: [ 'Ext.app.bind.Binding' ], isStub: true, dirty: true, formula: null, validationKey: 'validation', constructor: function(owner, name, parent) { var me = this, path = name; me.callParent([ owner, name ]); me.boundValue = null; if (parent) { parent.add(me); if (!parent.isRootStub) { path = parent.path + '.' + name; } me.checkHadValue(); } me.path = path; }, destroy: function() { var me = this, formula = me.formula, storeBinding = me.storeBinding; if (formula) { formula.destroy(); } if (storeBinding) { storeBinding.destroy(); } me.detachBound(); me.callParent(); }, bindValidation: function(callback, scope) { var parent = this.parent; return parent && parent.descend([this.validationKey, this.name]).bind(callback, scope); }, bindValidationField: function(callback, scope) { var parent = this.parent, name = this.name, lateBound = typeof callback === 'string', ret; if (parent) { ret = parent.bind(function(value) { var field = null; if (value && value.isModel) { field = value.getField(name); } if (lateBound) { scope[callback](field, value, this); } else { callback.call(scope, field, value, this); } }); } return ret || null; }, descend: function(path, index) { var me = this, children = me.children || (me.children = {}), pos = index || 0, name = path[pos++], ret; if (!(ret = children[name])) { ret = new Ext.app.bind.Stub(me.owner, name, me); } if (pos < path.length) { ret = ret.descend(path, pos); } return ret; }, getChildValue: function(parentData) { var me = this, name = me.name, bindMappings = me.bindMappings, storeMappings = bindMappings.store, modelMappings = bindMappings.model, ret; if (!parentData && !Ext.isString(parentData)) { // since these forms of falsey values (0, false, etc.) are not things we // can index into, this child stub must be null. ret = me.hadValue ? null : undefined; } else { ret = me.inspectValue(parentData); if (!ret) { if (parentData.isEntity) { // If we get here, we know it's not an association if (modelMappings[name]) { ret = parentData[modelMappings[name]](); } else { ret = parentData.data[name]; } } else if (parentData.isStore && storeMappings[name]) { ret = parentData[storeMappings[name]](); } else { ret = parentData[name]; if (ret === undefined && me.hadValue) { ret = null; } } } } return ret; }, getDataObject: function() { var me = this, parentData = me.parent.getDataObject(), // RootStub does not get here name = me.name, ret = parentData ? parentData[name] : null, storeMappings = me.bindMappings.store, associations; if (!ret) { if (parentData && parentData.isEntity) { // Check if the item is an association, if it is, grab it but don't load it. associations = parentData.associations; if (associations && name in associations) { ret = parentData[associations[name].getterName](); } } } else if (parentData.isStore && name in storeMappings) { ret = parentData[storeMappings[name]](); } if (!ret || !(ret.$className || Ext.isObject(ret))) { parentData[name] = ret = {}; // We're implicitly setting a value on the object here me.hadValue = true; // If we're creating the parent data object, invalidate the dirty // flag on our children. me.invalidate(true, true); } return ret; }, getRawValue: function() { // NOTE: The RootStub class does not call here so we will *always* have a parent // unless dark energy has won and the laws of physics have broken down. return this.getChildValue(this.getParentValue()); }, graft: function(replacement) { var me = this, parent = me.parent, children = me.children, name = me.name, i, ret; replacement.parent = parent; replacement.children = children; if (parent) { parent.children[name] = replacement; } if (children) { for (i in children) { children[i].parent = replacement; } } me.children = null; replacement.checkHadValue(); ret = me.callParent([ replacement ]); ret.invalidate(true, true); return ret; }, isAvailable: function() { return this.checkAvailability(); }, isLoading: function() { return !this.checkAvailability(true); }, invalidate: function(deep, dirtyOnly) { var me = this, children = me.children, name; me.dirty = true; me.checkHadValue(); if (!dirtyOnly && me.isAvailable()) { if (!me.scheduled) { // If we have no children, we're a leaf me.schedule(); } } if (deep && children) { for (name in children) { children[name].invalidate(deep, dirtyOnly); } } }, isReadOnly: function() { var formula = this.formula; return !!(formula && !formula.set); }, set: function(value, preventClimb) { var me = this, parent = me.parent, name = me.name, formula = me.formula, parentData, associations, association, formulaStub, setterName; if (formula && !formula.settingValue && formula.set) { formula.setValue(value); return; } else if (me.isLinkStub) { formulaStub = me.getLinkFormulaStub(); formula = formulaStub ? formulaStub.formula : null; if (formula) { //<debug> if (formulaStub.isReadOnly()) { Ext.raise('Cannot setValue on a readonly formula'); } //</debug> formula.setValue(value); return; } } // To set a child property, the parent must be an object... parentData = parent.getDataObject(); if (parentData.isEntity) { associations = parentData.associations; if (associations && (name in associations)) { association = associations[name]; setterName = association.setterName; if (setterName) { parentData[setterName](value); } // We may be setting a record here, force the value to recalculate me.invalidate(true); } else { // If not an association then it is a data field parentData.set(name, value); } // Setting fields or associated records will fire change notifications so we // handle the side effects there } else if ((value && value.constructor === Object) || !(value === parentData[name] && parentData.hasOwnProperty(name))) { // The hasOwnProperty check is important, even though the value might be the same here, // that value could exist in a viewmodel above us if (preventClimb || !me.setByLink(value)) { if (value === undefined) { delete parentData[name]; } else { parentData[name] = value; } me.inspectValue(parentData); // We have children, but we're overwriting the value with something else, so // we need to schedule our children me.invalidate(true); } } }, onStoreDataChanged: function() { this.invalidate(true); }, afterLoad: function(record) { this.invalidate(true); }, afterCommit: function(record) { // Essentially the same as an edit, but we don't know what changed. this.afterEdit(record, null); }, afterEdit: function(record, modifiedFieldNames) { var children = this.children, len = modifiedFieldNames && modifiedFieldNames.length, associations = record.associations, bindMappings = this.bindMappings.model, key, i, child, name, ref; // No point checking anything if we don't have children if (children) { if (len) { // We know what changed, check for it and schedule it. for (i = 0; i < len; ++i) { name = modifiedFieldNames[i]; child = children[name]; if (!child) { ref = record.fieldsMap[name]; ref = ref && ref.reference; child = ref && children[ref.role]; } if (child) { child.invalidate(true); } } } else { // We don't know what changed, so loop over everything. // If the child is not an association, then it's a field so we // need to trigger them so we can respond to field changes for (key in children) { if (!(associations && key in associations)) { children[key].invalidate(true); } } } // Whether we know what changed or not, valid/dirty are meta properties so // trigger them regardless for (key in bindMappings) { child = children[key]; if (child) { child.invalidate(); } } } this.invalidate(); }, afterReject: function(record) { // Essentially the same as an edit, but we don't know what changed. this.afterEdit(record, null); }, afterAssociatedRecordSet: function(record, associated, role) { var children = this.children, key = role.role; if (children && key in children) { children[key].invalidate(true); } }, setByLink: function(value) { var me = this, n = 0, ret = false, i, link, path, stub, root, name; for (stub = me; stub; stub = stub.parent) { if (stub.isLinkStub) { link = stub; if (n) { for (path = [], i = 0, stub = me; stub !== link; stub = stub.parent) { ++i; path[n - i] = stub.name; } } break; } ++n; } stub = null; if (link) { root = link.parent; name = link.name; if (!root.shouldClimb(name)) { // Write to root, descend to stub stub = root.insertChild(name); } else { stub = link.getTargetStub(); } } if (stub) { // We are a child of a link stub and that stub links to a Stub, so forward the set // call over there. This is needed to fire the bindings on that side of the link // and that will also arrive back here since we are a linked to it. if (path) { stub = stub.descend(path); } stub.set(value); ret = true; } return ret; }, setFormula: function(formula) { var me = this, oldFormula = me.formula; if (oldFormula) { oldFormula.destroy(); } // The new formula will bind to what it needs and that will schedule it (and then // us when it sets our value). me.formula = new Ext.app.bind.Formula(me, formula); }, react: function() { var me = this, bound = this.boundValue, children = me.children, generation; if (bound) { if (bound.isValidation) { bound.refresh(); generation = bound.generation; // Don't react if we haven't changed if (me.lastValidationGeneration === generation) { return; } me.lastValidationGeneration = generation; } else if (bound.isModel) { // At this point we're guaranteed to have a non-validation model // Check if we're interested in it, if so, validate it and let // the record fire off any changes if (children && children[me.validationKey]) { // Trigger validity checks bound.isValid(); } } } this.callParent(); }, privates: { bindMappings: { store: { count: 'getCount', first: 'first', last: 'last', loading: 'hasPendingLoad', totalCount: 'getTotalCount' }, model: { dirty: 'isDirty', phantom: 'isPhantom', valid: 'isValid' } }, checkAvailability: function(isLoading) { var me = this, parent = me.parent, bindMappings = me.bindMappings, name = me.name, available = !!(parent && parent.checkAvailability(isLoading)), associations, parentValue, value, availableSet; if (available) { parentValue = me.getParentValue(); value = me.inspectValue(parentValue); // If we get a value back, it's something we can ask for the loading state if (value) { if (isLoading) { available = !value.hasPendingLoad(); } else { // If it's a store, it should be always available, even if loading if (value.isStore) { available = true; } else { // If it's a model and it's loading, only available if it's after // the first time available = !value.isLoading() || value.loadCount > 0; } } } else { if (parentValue) { if (parentValue.isModel) { if (bindMappings.model[name]) { available = !parent.isLoading(); availableSet = true; } else { associations = parentValue.associations; // At this point, we know the value is not a record or a store, // otherwise something would have been returned from inspectValue. // We also check here that we are not a defined association, // because we don't treat it like a field. // Otherwise, we are a field on a model, so we're never in a loading // state. if (!(associations && name in associations)) { available = true; availableSet = true; } } } else if (parentValue.isStore && bindMappings.store[name] && name !== 'loading') { available = !parent.isLoading(); availableSet = true; } } if (!availableSet) { available = me.hadValue || me.getRawValue() !== undefined; } } } return available; }, checkHadValue: function() { if (!this.hadValue) { this.hadValue = this.getRawValue() !== undefined; } }, collect: function() { var me = this, result = me.callParent(), storeBinding = me.storeBinding ? 1 : 0, formula = me.formula ? 1 : 0; return result + storeBinding + formula; }, getLinkFormulaStub: function() { // Setting the value on a link backed by a formula should set the // formula. So we climb the hierarchy until we find the rootStub // and set it there if it be a formula. var stub = this; while (stub.isLinkStub) { stub = stub.binding.stub; } return stub.formula ? stub : null; }, getParentValue: function() { var me = this; // Cache the value of the parent here. Inside onSchedule we clear the value // because it may be invalidated. if (me.dirty) { me.parentValue = me.parent.getValue(); me.dirty = false; } return me.parentValue; }, setStore: function(storeBinding) { this.storeBinding = storeBinding; }, inspectValue: function(parentData) { var me = this, name = me.name, current = me.boundValue, boundValue = null, associations, raw, changed, associatedEntity; if (parentData && parentData.isEntity) { associations = parentData.associations; if (associations && (name in associations)) { boundValue = parentData[associations[name].getterName](); } else if (name === me.validationKey) { boundValue = parentData.getValidation(); // Binding a new one, reset the generation me.lastValidationGeneration = null; } } else if (parentData) { raw = parentData[name]; if (raw && (raw.isModel || raw.isStore)) { boundValue = raw; } } // Check if we have a current binding that changed. If so, we need // to detach ourselves from it changed = current !== boundValue; if (changed) { if (current) { me.detachBound(); } if (boundValue) { if (boundValue.isModel) { boundValue.join(me); } else { // Only want to trigger automatic loading if we've come from an association. // Otherwise leave the user in charge of that. associatedEntity = boundValue.associatedEntity; if (associatedEntity && boundValue.autoLoad !== false && !boundValue.complete && !boundValue.hasPendingLoad()) { boundValue.load(); } // We only want to listen for the first load, since the actual // store object won't change from then on boundValue.on({ scope: me, // Capture beginload/load so we can bind to the loading state // of the store. We need load because a load may be unsuccessful // which means datachanged won't fire beginload is used because // it's fired: // a) After we're sure to load (beforeload could be vetoed) // b) After the loading flag is set to true. This is important // because we fire the datachanged handler which needs to check if // the store is available (loading) to publish values. beginload: 'onStoreDataChanged', load: 'onStoreDataChanged', datachanged: 'onStoreDataChanged', destroy: 'onDestroyBound' }); } } me.boundValue = boundValue; } return boundValue; }, detachBound: function() { var me = this, current = me.boundValue; if (current && !current.destroyed) { if (current.isModel) { current.unjoin(me); } else { current.un({ scope: me, beginload: 'onStoreDataChanged', load: 'onStoreDataChanged', datachanged: 'onStoreDataChanged', destroy: 'onDestroyBound' }); } } }, onDestroyBound: function() { if (!this.owner.destroying) { this.set(null); } }, sort: function() { var me = this, formula = me.formula, scheduler = me.scheduler, storeBinding = me.storeBinding; me.callParent(); if (storeBinding) { scheduler.sortItem(storeBinding); } if (formula) { // Our formula must run before we do so it can set the value on us. Our // bindings in turn depend on us so they will be scheduled as part of the // current sweep if the formula produces a different result. scheduler.sortItem(formula); } } }});