UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

870 lines (796 loc) 28.8 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*eslint-disable max-len */ // Provides an abstract property binding. sap.ui.define([ "./BindingMode", "./ChangeReason", "./CompositeDataState", "./CompositeType", "./Context", "./PropertyBinding", "sap/base/assert", "sap/base/Log", "sap/base/util/deepEqual", "sap/ui/base/DataType", "sap/ui/base/SyncPromise" ], function(BindingMode, ChangeReason, CompositeDataState, CompositeType, Context, PropertyBinding, assert, Log, deepEqual, DataType, SyncPromise) { "use strict"; /** * Constructor for CompositeBinding. * * @class * Combines multiple property bindings (called 'parts') into a single one. * * A <code>CompositeBinding</code> combines the values from all its binding parts (each an * instance of <code>PropertyBinding</code>), either by calling a formatter function or by * involving a {@link sap.ui.model.CompositeType composite type}. When a formatter function is * used, the composite binding is automatically limited to <code>OneWay</code> mode. When a * type is used, the binding can also operate in <code>TwoWay</code> mode. * * Higher layers of the framework derive composite bindings from easy-to-write string * representations (the following features require complex binding syntax, e.g. * <code>data-sap-ui-bindingSyntax="complex"</code>): * * XML views, for example, convert attribute values with nested curly braces like * <pre> * text="{fullname} &amp;lt;{email}&amp;gt;" * </pre> * into a composite binding with two parts (one property binding for property "fullname" and one * for property "email") and with a generic formatter function that injects the values of the * parts into the string literal "{0} &lt;{1}&gt;" accordingly. * * Similarly, {@link topic:daf6852a04b44d118963968a1239d2c0 expression bindings} are parsed and * converted into composite bindings, too. The formatter function is created by the framework * and executes the calculations as defined by the expression string, taking the values from the * binding parts as input. * * <b>Note:</b> A nesting of composite bindings is currently not supported (albeit being * helpful). * * @see {@link topic:a2fe8e763014477e87990ff50657a0d0} * @public * @alias sap.ui.model.CompositeBinding * @extends sap.ui.model.PropertyBinding */ var CompositeBinding = PropertyBinding.extend("sap.ui.model.CompositeBinding", /** @lends sap.ui.model.CompositeBinding.prototype */ { constructor : function (aBindings, bRawValues, bInternalValues) { var oModel; PropertyBinding.apply(this, [null,""]); this.aBindings = aBindings; this.aValues = null; this.bRawValues = bRawValues; this.bPreventUpdate = false; this.bInternalValues = bInternalValues; this.bMultipleModels = this.aBindings.some(function (oBinding) { var oCurrentModel = oBinding.getModel(); oModel = oModel || oCurrentModel; return oModel && oCurrentModel && oCurrentModel !== oModel; }); }, metadata : { publicMethods : [ "getBindings", "attachChange", "detachChange" ] } }); CompositeBinding.prototype.destroy = function() { this.aBindings.forEach(function(oBinding) { oBinding.destroy(); }); PropertyBinding.prototype.destroy.apply(this); }; CompositeBinding.prototype.getPath = function() { assert(null, "Composite Binding has no path!"); return null; }; CompositeBinding.prototype.getModel = function() { assert(null, "Composite Binding has no model!"); return null; }; CompositeBinding.prototype.getContext = function() { assert(null, "Composite Binding has no context!"); return null; }; CompositeBinding.prototype.isResolved = function() { return this.aBindings.every(function(oBinding) { return oBinding.isResolved(); }); }; /** * Sets the optional type and internal type for the binding. The type and internal type are used * to do the parsing/formatting correctly. The internal type is the property type of the element * which the value is formatted to. * * @param {sap.ui.model.CompositeType} oType * The type for the binding * @param {string} sInternalType * The internal type of the element property which this binding is bound against. * * @public */ CompositeBinding.prototype.setType = function(oType, sInternalType) { var that = this; if (oType && !(oType instanceof CompositeType)) { throw new Error("Only CompositeType can be used as type for composite bindings!"); } PropertyBinding.prototype.setType.apply(this, arguments); // If a composite type is used, the type decides whether to use raw values or not if (this.oType) { oType.getPartsIgnoringMessages().forEach(function (i) { var oBinding = that.aBindings[i]; if (oBinding && oBinding.supportsIgnoreMessages() && oBinding.getIgnoreMessages() === undefined) { oBinding.setIgnoreMessages(true); } }); this.bRawValues = this.oType.getUseRawValues(); this.bInternalValues = this.oType.getUseInternalValues(); this.oType.processPartTypes(this.aBindings.map(function (oBinding) { return oBinding.getType(); })); if (this.bRawValues && this.bInternalValues) { throw new Error(this.oType + " has both 'bUseRawValues' & 'bUseInternalValues' set to true. Only one of them is allowed to be true"); } } }; /** * Sets the context for the property bindings of this composite binding which are either checked * thruthy via <code>mParameters.fnIsBindingRelevant</code> or whose model is the given * context's model. * * @param {sap.ui.model.Context} oContext * The new context for the bindings * @param {Object<string,any>} [mParameters] * Additional map of binding specific parameters * @param {function} [mParameters.fnIsBindingRelevant] * A callback function that checks whether the given context needs to be propagated to a * property binding of this composite binding. It gets the index of a property binding as * parameter and returns whether the given context needs to be propagated to that property * binding. */ CompositeBinding.prototype.setContext = function (oContext, mParameters) { var bCheckUpdate, bForceUpdate, aBindings = this.aBindings, oModel = oContext && oContext.getModel(), fnIsBindingRelevant = mParameters && mParameters.fnIsBindingRelevant ? mParameters.fnIsBindingRelevant : function (i) { // check if oModel is given since a context's model may have been destroyed return !oContext || oModel && oModel === aBindings[i].getModel(); }; aBindings.forEach(function (oBinding, i) { var oBindingContext; if (fnIsBindingRelevant(i)) { oBindingContext = oBinding.getContext(); bCheckUpdate = bCheckUpdate || oBinding.isRelative() && Context.hasChanged(oBindingContext, oContext); bForceUpdate = bForceUpdate || (bCheckUpdate && oBindingContext !== oContext); oBinding.setContext(oContext); } }); if (bCheckUpdate) { this.checkUpdate(bForceUpdate && this.getDataState().getControlMessages().length); } }; /** * Sets the values. This will cause the setValue to be called for each nested binding, except * for undefined values in the array. * * @param {array} aValues The values to set for this binding * * @public */ CompositeBinding.prototype.setValue = function(aValues) { if (this.bSuspended) { return; } this.aBindings.forEach(function(oBinding, i) { var oValue = aValues[i], sBindingMode = oBinding.getBindingMode(); if (oValue !== undefined && sBindingMode !== BindingMode.OneWay && sBindingMode !== BindingMode.OneTime) { oBinding.setValue(oValue); } }); this.getDataState().setValue(this.getValue()); }; /** * Returns the raw values of the property bindings in an array. * * @return {object} The values of the internal property bindings in an array * * @public */ CompositeBinding.prototype.getValue = function() { return this.aBindings.map(function(oBinding) { return oBinding.getValue(); }); }; CompositeBinding.prototype.getOriginalValue = function() { return this.aBindings.map(function(oBinding) { return oBinding.getDataState().getOriginalValue(); }); }; /** * Returns the current external value of the bound target which is formatted via a type or * formatter function. * * @return {object} The current value of the bound target * * @throws {sap.ui.model.FormatException} * * @public */ CompositeBinding.prototype.getExternalValue = function() { var aValues = [], oInternalType, oValue; switch (this.sInternalType) { case "raw": return this.getRawValue(); case "internal": return this.getInternalValue(); default: oInternalType = this.sInternalType && DataType.getType(this.sInternalType); aValues = this.getCurrentValues(); if (this.fnFormatter) { oValue = this.fnFormatter.apply(this, aValues); } else if (this.oType) { oValue = this.oType.formatValue(aValues, this.sInternalType); } else if (oInternalType instanceof DataType && oInternalType.isArrayType()) { oValue = aValues; } else if (aValues.length > 1) { // default: multiple values are joined together as space separated list if no formatter or type specified oValue = aValues.join(" "); } else { oValue = aValues[0]; } return oValue; } }; /** * Sets the external value of a composite binding. If no CompositeType is assigned to the * binding, the default implementation assumes a space-separated list of values. This will cause * the setValue to be called for each nested binding, except for undefined values in the array. * * @param {object} oValue The value to set for this binding * @return {undefined|Promise} A promise in case of asynchronous type parsing or validation * * @throws sap.ui.model.ParseException * @throws sap.ui.model.ValidateException * * @public */ CompositeBinding.prototype.setExternalValue = function(oValue) { var oInternalType, oDataState, vResult, pValues, that = this; if (this.sInternalType === "raw") { this.setRawValue(oValue); return undefined; } else if (this.sInternalType === "internal") { this.setInternalValue(oValue); return undefined; } oInternalType = this.sInternalType && DataType.getType(this.sInternalType); // No twoway binding when using formatters if (this.fnFormatter) { Log.warning("Tried to use twoway binding, but a formatter function is used"); return undefined; } oDataState = this.getDataState(); if (this.oType) { pValues = SyncPromise.resolve().then(function() { var aCurrentValues; if (that.oType.getParseWithValues()) { aCurrentValues = that.getCurrentValues(); } return that.oType.parseValue(oValue, that.sInternalType, aCurrentValues); }).then(function(aValues) { var aValidateValues = that.getValidateValues(aValues); return SyncPromise.all([aValues, that.oType.validateValue(aValidateValues)]); }).then(function(aResult) { return aResult[0]; }).catch(function(oException) { oDataState.setInvalidValue(oValue); that.checkDataState(); //data ui state is dirty inform the control throw oException; }); } else if (Array.isArray(oValue) && oInternalType instanceof DataType && oInternalType.isArrayType()) { pValues = SyncPromise.resolve(oValue); } else if (typeof oValue == "string") { // default: multiple values are split by space character together if no formatter or type specified pValues = SyncPromise.resolve(oValue.split(" ")); } else { pValues = SyncPromise.resolve([oValue]); } vResult = pValues.then(function(aValues) { that.aBindings.forEach(function(oBinding, iIndex) { var sBindingMode = oBinding.getBindingMode(); oValue = aValues[iIndex]; // if a value is undefined skip the update of the nestend binding - this allows partial updates if (oValue !== undefined && sBindingMode !== BindingMode.OneWay && sBindingMode !== BindingMode.OneTime) { if (that.bRawValues) { oBinding.setRawValue(oValue); } else if (that.bInternalValues) { oBinding.setInternalValue(oValue); } else { oBinding.setExternalValue(oValue); } } }); oDataState.setValue(that.getValue()); oDataState.setInvalidValue(undefined); }); vResult.catch(function () {/*avoid "Uncaught (in promise)"*/}); return vResult.unwrap(); }; /** * Returns the current internal value of the bound target which is an array of the internal (JS * native) values of nested bindings. * * @return {array} The current values of the nested bindings * * @public */ CompositeBinding.prototype.getInternalValue = function() { return this.aBindings.map(function(oBinding) { return oBinding.getInternalValue(); }); }; /** * Sets the internal value of the bound target. Parameter must be an array of values matching * the internal (JS native) types of nested bindings. * * @param {array} aValues the new values of the nested bindings * @return {undefined|Promise} A promise in case of asynchronous type parsing or validation * * @public */ CompositeBinding.prototype.setInternalValue = function(aValues) { var oDataState = this.getDataState(), pValues, that = this; if (this.oType) { pValues = SyncPromise.resolve(aValues).then(function(aValidateValues){ if (!that.bInternalValues) { aValidateValues = that.aBindings.map(function(oBinding, i) { return oBinding._internalToRaw(aValidateValues[i]); }); if (!that.bRawValues) { aValidateValues = that.aBindings.map(function(oBinding, i) { return oBinding._rawToExternal(aValidateValues[i]); }); } } return that.oType.validateValue(aValidateValues); }).then(function() { return aValues; }).catch(function(oException) { oDataState.setInvalidValue(aValues); that.checkDataState(); //data ui state is dirty inform the control throw oException; }); } else { pValues = SyncPromise.resolve(aValues); } return pValues.then(function() { that.aBindings.forEach(function(oBinding, iIndex) { var vValue = aValues[iIndex], sBindingMode = oBinding.getBindingMode(); if (vValue !== undefined && sBindingMode !== BindingMode.OneWay && sBindingMode !== BindingMode.OneTime) { oBinding.setInternalValue(vValue); } }); oDataState.setValue(that.getValue()); oDataState.setInvalidValue(undefined); }).unwrap(); }; /** * Returns the current raw value of the bound target which is an array of the raw (model) * values of nested bindings. * * @return {array} The current values of the nested bindings * * @public */ CompositeBinding.prototype.getRawValue = function() { return this.aBindings.map(function(oBinding) { return oBinding.getRawValue(); }); }; /** * Sets the raw value of the bound target. Parameter must be an array of values matching the raw * (model) types of nested bindings. * * @param {array} aValues the new values of the nested bindings * @return {undefined|Promise} A promise in case of asynchronous type parsing or validation * * @public */ CompositeBinding.prototype.setRawValue = function(aValues) { var oDataState = this.getDataState(), pValues, that = this; if (this.oType) { pValues = SyncPromise.resolve(aValues).then(function(aValidateValues){ if (!that.bRawValues) { if (that.bInternalValues) { aValidateValues = that.aBindings.map(function(oBinding, i) { return oBinding._rawToInternal(aValidateValues[i]); }); } else { aValidateValues = that.aBindings.map(function(oBinding, i) { return oBinding._rawToExternal(aValidateValues[i]); }); } } return that.oType.validateValue(aValidateValues); }).then(function() { return aValues; }).catch(function(oException) { oDataState.setInvalidValue(aValues); that.checkDataState(); //data ui state is dirty inform the control throw oException; }); } else { pValues = SyncPromise.resolve(aValues); } return pValues.then(function() { that.aBindings.forEach(function(oBinding, iIndex) { var vValue = aValues[iIndex], sBindingMode = oBinding.getBindingMode(); if (vValue !== undefined && sBindingMode !== BindingMode.OneWay && sBindingMode !== BindingMode.OneTime) { oBinding.setRawValue(vValue); } }); oDataState.setValue(that.getValue()); oDataState.setInvalidValue(undefined); }).unwrap(); }; /** * Returns an array with the current values as available in the bindings. * Depending on the raw/internal value flags, this may return raw/internal values. * * @return {array} the values of all bindings * * @private */ CompositeBinding.prototype.getCurrentValues = function() { if (this.bRawValues) { return this.getRawValue(); } else if (this.bInternalValues) { return this.getInternalValue(); } else { return this.aBindings.map(function(oBinding) { return oBinding.getExternalValue(); }); } }; /** * Returns values to validate. In case the value array does contain undefined values * they will be filled with actual data of nested bindings. This ensures the validate * method always gets the full set of values to validate, even if partial updates are * used. * * @param {any[]} aValues Values to fill empty current values with * @return {array} Array of values used for validation * * @private */ CompositeBinding.prototype.getValidateValues = function(aValues) { var aCurrentValues, bPartialUpdate, aValidateValues = aValues; bPartialUpdate = this.aBindings.some(function(vPart, i) { return aValues[i] === undefined; }); if (bPartialUpdate) { aCurrentValues = this.getCurrentValues(); aValidateValues = aCurrentValues.map(function(vValue, i) { return aValues[i] === undefined ? vValue : aValues[i]; }); } return aValidateValues; }; /** * Returns the property bindings contained in this composite binding. * * @return {array} The property bindings in this composite binding * * @public */ CompositeBinding.prototype.getBindings = function() { return this.aBindings; }; /** * {@see sap.ui.model.Binding#hasValidation} * * @returns {boolean} * Whether the binding throws a validation exception when an invalid value is set on it * @private */ CompositeBinding.prototype.hasValidation = function() { if (this.getType()) { // If the CompositeBinding has a type of its own, it always validates return true; } // If the one of the inner bindings has a type of its own the CompositeBinding validates var aBindings = this.getBindings(); for (var i = 0; i < aBindings.length; ++i) { if (aBindings[i].hasValidation()) { return true; } } return false; }; //Eventing and related /** * Attaches event handler <code>fnFunction</code> to the <code>change</code> event of this * <code>sap.ui.model.CompositeBinding</code>. * * When called, the context of the event handler (its <code>this</code>) will be bound to * <code>oListener</code> if specified, otherwise it will be bound to this * <code>sap.ui.model.CompositeBinding</code> itself. * * @param {function} fnFunction The function to be called, when the event occurs * @param {object} [oListener] Object on which to call the given function * @protected */ CompositeBinding.prototype.attachChange = function(fnFunction, oListener) { var that = this; this.fChangeHandler = function(oEvent) { if (that.bSuspended) { return; } var oBinding = oEvent.getSource(); if (oBinding.getBindingMode() == BindingMode.OneTime) { oBinding.detachChange(that.fChangeHandler); } /*bForceUpdate true gets lost (e.g. checkUpdate(true) on model); But if an embedded binding fires a change we could * call checkUpdate(true) so we handle both cases: a value change of the binding and a checkUpdate(true) */ that.checkUpdate(true); }; this.attachEvent("change", fnFunction, oListener); if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.attachChange(that.fChangeHandler); }); } }; /** * Detaches event handler <code>fnFunction</code> from the <code>change</code> event of this * <code>sap.ui.model.CompositeBinding</code>. * * @param {function} fnFunction The function to be called, when the event occurs * @param {object} [oListener] Object on which to call the given function * @protected */ CompositeBinding.prototype.detachChange = function(fnFunction, oListener) { var that = this; this.detachEvent("change", fnFunction, oListener); if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.detachChange(that.fChangeHandler); }); } }; /** * Attaches event handler <code>fnFunction</code> to the <code>DataStateChange</code> event of * this <code>sap.ui.model.CompositeBinding</code>. * * When called, the context of the event handler (its <code>this</code>) will be bound to * <code>oListener</code> if specified, otherwise it will be bound to this * <code>sap.ui.model.CompositeBinding</code> itself. * * @param {function} fnFunction The function to be called, when the event occurs * @param {object} [oListener] Object on which to call the given function * @protected */ CompositeBinding.prototype.attachDataStateChange = function(fnFunction, oListener) { var that = this; this.fDataStateChangeHandler = function(oEvent) { var oBinding = oEvent.getSource(); if (oBinding.getBindingMode() == BindingMode.OneTime) { oBinding.detachDataStateChange(that.fChangeHandler); } that.checkDataState(); }; this.attachEvent("DataStateChange", fnFunction, oListener); if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.attachEvent("DataStateChange", that.fDataStateChangeHandler); }); } }; /** * Detaches event handler <code>fnFunction</code> from the <code>DataStateChange</code> event of * this <code>sap.ui.model.CompositeBinding</code>. * * @param {function} fnFunction The function to be called, when the event occurs * @param {object} [oListener] Object on which to call the given function * @protected */ CompositeBinding.prototype.detachDataStateChange = function(fnFunction, oListener) { var that = this; this.detachEvent("DataStateChange", fnFunction, oListener); if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.detachEvent("DataStateChange", that.fDataStateChangeHandler); }); } }; /** * Attaches event handler <code>fnFunction</code> to the <code>AggregatedDataStateChange</code> * event of this <code>sap.ui.model.CompositeBinding</code>. * * The <code>AggregatedDataStateChange</code> event is fired asynchronously, meaning that the * <code>DataState</code> object given as parameter of the event contains all changes that were * applied to the <code>DataState</code> in the running thread. * * @param {function} fnFunction The function to be called, when the event occurs * @param {object} [oListener] Object on which to call the given function * @protected */ CompositeBinding.prototype.attachAggregatedDataStateChange = function(fnFunction, oListener) { var that = this; if (!this.fDataStateChangeHandler) { this.fDataStateChangeHandler = function(oEvent) { var oBinding = oEvent.getSource(); if (oBinding.getBindingMode() == BindingMode.OneTime) { oBinding.detachDataStateChange(that.fChangeHandler); } that.checkDataState(); }; } this.attachEvent("AggregatedDataStateChange", fnFunction, oListener); if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.attachEvent("DataStateChange", that.fDataStateChangeHandler); }); } }; /** * Detaches event handler <code>fnFunction</code> from the * <code>AggregatedDataStateChange</code> event of this * <code>sap.ui.model.CompositeBinding</code>. * * @param {function} fnFunction The function to be called, when the event occurs * @param {object} [oListener] Object on which to call the given function * @protected */ CompositeBinding.prototype.detachAggregatedDataStateChange = function(fnFunction, oListener) { var that = this; this.detachEvent("AggregatedDataStateChange", fnFunction, oListener); if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.detachEvent("DataStateChange", that.fDataStateChangeHandler); }); } }; /** * Determines if the property bindings in the composite binding should be updated by calling * updateRequired on all property bindings with the specified model. * * @param {object} oModel The model instance to compare against * @returns {boolean} Whether this binding should be updated * @protected */ CompositeBinding.prototype.updateRequired = function(oModel) { var bUpdateRequired = false; this.aBindings.forEach(function(oBinding) { bUpdateRequired = bUpdateRequired || oBinding.updateRequired(oModel); }); return bUpdateRequired; }; /** * Initialize the binding. The method should be called when creating a binding. * The default implementation calls checkUpdate(true). * Prevent checkUpdate to be triggered while initializing nested bindings, it is * sufficient to call checkUpdate when all nested bindings are initialized. * * @returns {this} A reference to itself * * @protected */ CompositeBinding.prototype.initialize = function() { this.bPreventUpdate = true; if (this.aBindings) { this.aBindings.forEach(function(oBinding) { oBinding.initialize(); }); } this.bPreventUpdate = false; if (!this.bSuspended) { this.checkUpdate(true); } return this; }; /** * Returns the data state for this binding. * * @return {sap.ui.model.CompositeDataState} The data state */ CompositeBinding.prototype.getDataState = function() { if (!this.oDataState) { this.oDataState = new CompositeDataState(this.aBindings.map(function(oBinding) { return oBinding.getDataState(); })); } return this.oDataState; }; /** * Suspends the binding update. No change events will be fired. * * A refresh call with bForceUpdate set to true will also update the binding and fire a change * in suspended mode. Special operations on bindings, which require updates to work properly * (as paging or filtering in list bindings) will also update and cause a change event although * the binding is suspended. * * @public */ CompositeBinding.prototype.suspend = function() { this.bSuspended = true; this.aBindings.forEach(function(oBinding) { oBinding.suspend(); }); }; /** * Suspends the binding update. No change events will be fired. * * A refresh call with bForceUpdate set to true will also update the binding and fire a change * in suspended mode. Special operations on bindings, which require updates to work properly * (as paging or filtering in list bindings) will also update and cause a change event although * the binding is suspended. * * @public */ CompositeBinding.prototype.resume = function() { this.aBindings.forEach(function(oBinding) { oBinding.resume(); }); this.bSuspended = false; this.checkUpdate(true); }; /** * Check whether this Binding would provide new values and in case it changed, * inform interested parties about this. * * @param {boolean} [bForceUpdate] Whether an update should be forced * */ CompositeBinding.prototype.checkUpdate = function(bForceUpdate){ var bChanged = false; if (this.bPreventUpdate || (this.bSuspended && !bForceUpdate)) { return; } // do not fire change event in case the destruction of the model for one part leads to the // update of a model for another part of this binding if (this.bMultipleModels && this.aBindings.some(function (oBinding) { var oModel = oBinding.getModel(); return oModel && oModel.bDestroyed; })) { return; } var oDataState = this.getDataState(); var aOriginalValues = this.getOriginalValue(); if (bForceUpdate || !deepEqual(aOriginalValues, this.aOriginalValues)) { this.aOriginalValues = aOriginalValues; oDataState.setOriginalValue(aOriginalValues); bChanged = true; } var aValues = this.getValue(); if (!deepEqual(aValues, this.aValues) || bForceUpdate) {// optimize for not firing the events when unneeded this.aValues = aValues; oDataState.setValue(aValues); this._fireChange({reason: ChangeReason.Change}); bChanged = true; } if (bChanged) { this.checkDataState(); } }; return CompositeBinding; });