@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
589 lines (544 loc) • 22.6 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/*eslint-disable max-len */
/**
* JSON-based DataBinding
*
* @namespace
* @name sap.ui.model.json
* @public
*/
// Provides the JSON object based model implementation
sap.ui.define([
"./JSONListBinding",
"./JSONPropertyBinding",
"./JSONTreeBinding",
"sap/base/Log",
"sap/base/util/deepExtend",
"sap/base/util/isPlainObject",
"sap/ui/model/ClientModel",
"sap/ui/model/Context"
], function(JSONListBinding, JSONPropertyBinding, JSONTreeBinding, Log, deepExtend, isPlainObject,
ClientModel, Context) {
"use strict";
/**
* Constructor for a new JSONModel.
*
* When observation is activated, the application can directly change the JS objects without the need to call
* {@link sap.ui.model.json.JSONModel#setData}, {@link sap.ui.model.json.JSONModel#setProperty} or
* {@link sap.ui.model.Model#refresh}. <b>Note:</b> Observation only works for existing properties in the JSON
* model. Newly added or removed properties and newly added or removed array entries, for example, are not detected.
*
* @param {object|string} [oData] Either the URL where to load the JSON from or a JS object
* @param {boolean} [bObserve=false] Whether to observe the JSON data for property changes
*
* @class
* Model implementation for the JSON format.
*
* This model is not prepared to be inherited from.
*
* The model does not support {@link sap.ui.model.json.JSONModel#bindList binding lists} in case the bound data
* contains circular structures and the bound control uses
* {@link topic:7cdff73f308b4b10bdf7d83b7aba72e7 extended change detection}.
*
* @extends sap.ui.model.ClientModel
*
* @author SAP SE
* @version 1.147.0
* @public
* @alias sap.ui.model.json.JSONModel
*/
var JSONModel = ClientModel.extend("sap.ui.model.json.JSONModel", /** @lends sap.ui.model.json.JSONModel.prototype */ {
constructor : function(oData, bObserve) {
// init promise before ClientModel c'tor as it calls #loadData
this.pSequentialImportCompleted = Promise.resolve();
ClientModel.apply(this, arguments);
this.bObserve = bObserve;
if (oData && typeof oData == "object") {
this.setData(oData);
}
},
metadata : {
publicMethods : ["setJSON", "getJSON"]
}
});
/**
* Returns a copy of all active bindings of the model.
*
* @return {sap.ui.model.Binding[]} The active bindings of the model
*
* @function
* @name sap.ui.model.json.JSONModel.prototype.getBindings
* @private
* @ui5-restricted sap.ushell
*/
/**
* Sets the data, passed as a JS object tree, to the model.
*
* @param {object} oData the data to set on the model
* @param {boolean} [bMerge=false] whether to merge the data instead of replacing it
* @throws {Error} If the provided data contains a cycle and <code>bMerge</code> is set
*
* @public
*/
JSONModel.prototype.setData = function(oData, bMerge){
if (bMerge) {
// do a deep copy
this.oData = deepExtend(Array.isArray(this.oData) ? [] : {}, this.oData, oData);
} else {
this.oData = oData;
}
if (this.bObserve) {
this.observeData();
}
this.checkUpdate();
};
/**
* Recursively iterates the JSON data and adds setter functions for the properties
*
* @private
*/
JSONModel.prototype.observeData = function(){
var that = this;
function createGetter(vValue) {
return function() {
return vValue;
};
}
function createSetter(oObject, sName) {
return function(vValue) {
// Newly added data needs to be observed to be included
observeRecursive(vValue, oObject, sName);
that.checkUpdate();
};
}
function createProperty(oObject, sName, vValue) {
// Do not create getter/setter for function references
if (typeof vValue == "function"){
oObject[sName] = vValue;
} else {
Object.defineProperty(oObject, sName, {
get: createGetter(vValue),
set: createSetter(oObject, sName)
});
}
}
function observeRecursive(oObject, oParentObject, sName) {
var i;
if (Array.isArray(oObject)) {
for (i = 0; i < oObject.length; i++) {
observeRecursive(oObject[i], oObject, i);
}
} else if (isPlainObject(oObject)) {
for (i in oObject) {
observeRecursive(oObject[i], oObject, i);
}
}
if (oParentObject) {
createProperty(oParentObject, sName, oObject);
}
}
observeRecursive(this.oData);
};
/**
* Sets the data, passed as a string in JSON format, to the model.
*
* @param {string} sJSON the JSON data to set on the model
* @param {boolean} [bMerge=false] whether to merge the data instead of replacing it
*
* @public
*/
JSONModel.prototype.setJSON = function(sJSON, bMerge){
var oJSONData;
try {
oJSONData = JSON.parse(sJSON + "");
this.setData(oJSONData, bMerge);
} catch (e) {
Log.fatal("The following problem occurred: JSON parse Error: " + e);
this.fireParseError({url : "", errorCode : -1,
reason : "", srcText : e, line : -1, linepos : -1, filepos : -1});
}
};
/**
* Serializes the current JSON data of the model into a string.
*
* @return {string} The JSON data serialized as string
* @public
*/
JSONModel.prototype.getJSON = function(){
return JSON.stringify(this.oData);
};
/**
* Loads JSON-encoded data from the server and stores the resulting JSON data in the model.
* Note: Due to browser security restrictions, most "Ajax" requests are subject to the same origin policy,
* the request can not successfully retrieve data from a different domain, subdomain, or protocol.
*
* Note: To send a JSON object in the body of a "POST" request to load the model data, <code>oParameters</code> has
* to be the JSON-stringified value of the object to be sent, and <code>mHeaders</code> has to contain a
* <code>"Content-Type"</code> property with the value <code>"application/json;charset=utf-8"</code>.
*
* @param {string} sURL A string containing the URL to which the request is sent
* @param {object | string} [oParameters]
* The data to be sent to the server with the data-loading request. If <code>oParameters</code> is a string, it
* has to be encoded based on the used content type. The default encoding is
* <code>'application/x-www-form-urlencoded; charset=UTF-8'</code> but it may be overwritten via the
* <code>"Content-Type"</code> property given in <code>mHeaders</code>. If <code>oParameters</code> is an object,
* a string is generated and the keys and values are URL-encoded. The resulting string is appended to the URL if
* the HTTP request method cannot have a request body, e.g. for a "GET" request. Otherwise, the resulting string
* is added to the request body.
* @param {boolean} [bAsync=true] <b>Deprecated as of Version 1.107</b>; always use asynchronous
* loading for performance reasons. By default, all requests are sent asynchronously.
* Synchronous requests may temporarily lock the browser, disabling any actions while
* the request is active. Cross-domain requests do not support synchronous operations.
* @param {string} [sType="GET"] The HTTP verb to use for the request ("GET" or "POST")
* @param {boolean} [bMerge=false] Whether the data should be merged instead of replaced
* @param {boolean} [bCache=true] <b>Deprecated as of Version 1.107</b>; always use the cache
* headers from the back-end system for performance reasons. Disables caching if set to
* <code>false</code>.
* @param {object} [mHeaders] An object of additional header key/value pairs to send along with the request
*
* @return {Promise|undefined} in case bAsync is set to true a Promise is returned; this promise resolves/rejects based on the request status
* @public
*/
JSONModel.prototype.loadData = function(sURL, oParameters, bAsync, sType, bMerge, bCache, mHeaders){
var pImportCompleted;
bAsync = (bAsync !== false);
sType = sType || "GET";
bCache = bCache === undefined ? this.bCache : bCache;
this.fireRequestSent({url : sURL, type : sType, async : bAsync, headers: mHeaders,
info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}});
var fnSuccess = function(oData) {
if (!oData) {
Log.fatal("The following problem occurred: No data was retrieved by service: " + sURL);
}
this.setData(oData, bMerge);
this.fireRequestCompleted({url : sURL, type : sType, async : bAsync, headers: mHeaders,
info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}, success: true});
}.bind(this);
var fnError = function(oParams, sTextStatus){
// the textStatus is either passed by jQuery via arguments,
// or by us from a promise reject() in the async case
var sMessage = sTextStatus || oParams.textStatus;
var oParameters = bAsync ? oParams.request : oParams;
var iStatusCode = oParameters.status;
var sStatusText = oParameters.statusText;
var sResponseText = oParameters.responseText;
var oError = {
message : sMessage,
statusCode : iStatusCode,
statusText : sStatusText,
responseText : sResponseText
};
Log.fatal("The following problem occurred: " + sMessage, sResponseText + "," + iStatusCode + "," + sStatusText);
this.fireRequestCompleted({url : sURL, type : sType, async : bAsync, headers: mHeaders,
info : "cache=" + bCache + ";bMerge=" + bMerge, infoObject: {cache : bCache, merge : bMerge}, success: false, errorobject: oError});
this.fireRequestFailed(oError);
if (bAsync) {
return Promise.reject(oError);
}
return undefined;
}.bind(this);
var _loadData = function(fnSuccess, fnError) {
this._ajax({
url: sURL,
async: bAsync,
dataType: 'json',
cache: bCache,
data: oParameters,
headers: mHeaders,
jsonp: false,
type: sType,
success: fnSuccess,
error: fnError
});
}.bind(this);
if (bAsync) {
pImportCompleted = new Promise(function(resolve, reject) {
var fnReject = function(oXMLHttpRequest, sTextStatus, oError) {
reject({request: oXMLHttpRequest, textStatus: sTextStatus, error: oError});
};
_loadData(resolve, fnReject);
});
// chain the existing loadData calls, so the import is done sequentially
var pReturn = this.pSequentialImportCompleted.then(function() {
return pImportCompleted.then(fnSuccess, fnError);
});
// attach exception/rejection handler, so the internal import promise always resolves
this.pSequentialImportCompleted = pReturn.catch(function(oError) {
Log.error("Loading of data failed: " + sURL);
});
// return chained loadData promise (sequential imports)
// but without a catch handler, so the application can also is notified about request failures
return pReturn;
} else {
_loadData(fnSuccess, fnError);
return undefined;
}
};
/**
* Returns a Promise of the current data-loading state.
* Every currently running {@link sap.ui.model.json.JSONModel#loadData} call is respected by the returned Promise.
* This also includes a potential loadData call from the JSONModel's constructor in case a URL was given.
* The data-loaded Promise will resolve once all running requests have finished.
* Only requests, which have been queued up to the point of calling
* this function will be respected by the returned Promise.
*
* @return {Promise<undefined>} a Promise, which resolves if all pending data-loading requests have finished
* @public
*/
JSONModel.prototype.dataLoaded = function() {
return this.pSequentialImportCompleted;
};
/**
* Creates a new {@link sap.ui.model.json.JSONPropertyBinding}.
*
* @param {string} sPath
* The path to the property to bind
* @param {sap.ui.model.Context} [oContext]
* The context for the binding. This is mandatory when a relative binding path is provided.
* @param {Object<string, any>} [mParameters]
* Additional model-specific parameters
* @param {boolean} [mParameters.ignoreMessages]
* Whether this binding ignores model messages instead of propagating them to the control. Supported since
* 1.119.0. Some composite types like {@link sap.ui.model.type.Currency} automatically ignore
* model messages for some of their parts, depending on their format options. Setting this
* parameter to <code>true</code> or <code>false</code> overrules the automatism of the type.
*
* <b>Example:</b> A binding for a currency code is used in a composite binding for rendering the
* proper number of decimals, but the currency code itself is not displayed in the attached control.
* In this case, messages for the currency code aren't displayed at that control, only
* messages for the amount.
* @returns {sap.ui.model.json.JSONPropertyBinding}
* The newly created JSONPropertyBinding
*
* @public
*/
JSONModel.prototype.bindProperty = function(sPath, oContext, mParameters) {
var oBinding = new JSONPropertyBinding(this, sPath, oContext, mParameters);
return oBinding;
};
/**
* Creates a new {@link sap.ui.model.json.JSONListBinding}.
*
* @param {string} sPath
* The path to the list or array to bind
* @param {sap.ui.model.Context} [oContext]
* The context for the binding. This is mandatory when a relative binding path is provided.
* @param {sap.ui.model.Sorter[]|sap.ui.model.Sorter} [aSorters=[]]
* The sorters used initially. To replace them, call {@link sap.ui.model.ListBinding#sort}.
* @param {sap.ui.model.Filter[]|sap.ui.model.Filter} [aFilters=[]]
* The filters initially used with type {@link sap.ui.model.FilterType.Application}.
* To replace them, call {@link sap.ui.model.ListBinding#filter}.
* @param {Object<string, any>} [mParameters]
* Map of optional parameters as defined by subclasses. This class does not introduce any own parameters.
* @returns {sap.ui.model.json.JSONListBinding}
* The newly created JSONListBinding
* @throws {Error}
* If the {@link sap.ui.model.Filter.NONE} filter instance is contained in <code>aFilters</code> together with
* other filters
*
* @public
*/
JSONModel.prototype.bindList = function(sPath, oContext, aSorters, aFilters, mParameters) {
var oBinding = new JSONListBinding(this, sPath, oContext, aSorters, aFilters, mParameters);
return oBinding;
};
/**
* Creates a new {@link sap.ui.model.json.JSONTreeBinding}.
* The bound data can contain JSON objects and arrays.
* Both are used to build the tree structure.
*
* @param {string} sPath
* The path pointing to the tree or array that is bound
* @param {sap.ui.model.Context} [oContext]
* The context for the binding. This is mandatory when a relative binding path is provided.
* @param {sap.ui.model.Filter[]|sap.ui.model.Filter} [aFilters=[]]
* The filters initially used with type {@link sap.ui.model.FilterType.Application}.
* To replace them, call {@link sap.ui.model.TreeBinding#filter}.
* @param {Object<string, any>} [mParameters]
* Additional model-specific parameters
* @param {string[]} [mParameters.arrayNames]
* Keys of arrays to be used for building the tree structure. If not specified, all arrays and objects in the
* bound data are used.
* Note: For arrays nested within other arrays with different names, add both array names to
* <code>arrayNames</code>. To include a nested array in the hierarchy, you must list the names of all containing
* arrays. If an array name is missing from the list, its child arrays are also excluded from the hierarchy, even
* if you add them to <code>arrayNames</code>.
* If this parameter is set, all other objects and arrays in the bound data are ignored.
* @param {sap.ui.model.Sorter[]|sap.ui.model.Sorter} [aSorters=[]]
* The sorters used initially. To replace them, call {@link sap.ui.model.TreeBinding#sort}.
* @returns {sap.ui.model.json.JSONTreeBinding}
* The newly created JSONTreeBinding
* @throws {Error}
* If the <code>aFilters</code> array contains the {@link sap.ui.model.Filter.NONE} filter instance together with
* other filters
*
* @public
*/
JSONModel.prototype.bindTree = function(sPath, oContext, aFilters, mParameters, aSorters) {
var oBinding = new JSONTreeBinding(this, sPath, oContext, aFilters, mParameters, aSorters);
return oBinding;
};
/**
* Sets <code>oValue</code> as new value for the property defined by the given
* <code>sPath</code> and <code>oContext</code>. Once the new model value has been set, all
* interested parties are informed.
*
* Consecutive calls of this method which update bindings <em>synchronously</em> may cause performance issues; see
* {@link topic:18a76b577b144bc2b9b424e39d379c06 Performance Impact of Model Updates} for details.
*
* @param {string} sPath
* The path of the property to set
* @param {any} oValue
* The new value to be set for this property
* @param {sap.ui.model.Context} [oContext]
* The context used to set the property
* @param {boolean} [bAsyncUpdate]
* Whether to update bindings dependent on this property asynchronously
* @return {boolean}
* <code>true</code> if the value was set correctly, and <code>false</code> if errors
* occurred, for example if the entry was not found.
* @public
*/
JSONModel.prototype.setProperty = function(sPath, oValue, oContext, bAsyncUpdate) {
var sResolvedPath = this.resolve(sPath, oContext),
iLastSlash, sObjectPath, sProperty;
// return if path / context is invalid
if (!sResolvedPath) {
return false;
}
// If data is set on root, call setData instead
if (sResolvedPath == "/") {
this.setData(oValue);
return true;
}
iLastSlash = sResolvedPath.lastIndexOf("/");
// In case there is only one slash at the beginning, sObjectPath must contain this slash
sObjectPath = sResolvedPath.substring(0, iLastSlash || 1);
sProperty = sResolvedPath.substr(iLastSlash + 1);
var oObject = this._getObject(sObjectPath);
if (oObject) {
oObject[sProperty] = oValue;
const iUpdatedBindings = this.checkUpdate(false, bAsyncUpdate);
this.checkPerformanceOfUpdate(iUpdatedBindings, bAsyncUpdate);
return true;
}
return false;
};
/**
* Returns the value for the given <code>sPath</code> and <code>oContext</code>.
*
* @param {string} sPath
* The path to the property
* @param {sap.ui.model.Context} [oContext]
* The context which will be used to retrieve the property
* @return {any|null|undefined}
* The value of the property. If the property is not found, <code>null</code> or
* <code>undefined</code> is returned.
* @public
*/
JSONModel.prototype.getProperty = function(sPath, oContext) {
return this._getObject(sPath, oContext);
};
/**
* Returns the value for the given <code>sPath</code> and <code>oContext</code>.
*
* @param {string} sPath
* The path to the property
* @param {object|sap.ui.model.Context} [oContext]
* The context or a JSON object
* @returns {any|null|undefined}
* The value of the property. If the property path derived from the given path and context is
* absolute (starts with a "/") but does not lead to a property in the data structure,
* <code>undefined</code> is returned. If the property path is not absolute, <code>null</code>
* is returned.
*
* Note: If a JSON object is given instead of a context, the value of the property is taken
* from the JSON object. If the given path does not lead to a property, <code>undefined</code>
* is returned. If the given path represents a falsy JavaScript value, the given JSON object
* is returned.
* @private
*/
JSONModel.prototype._getObject = function (sPath, oContext) {
let oNode = null;
/** @deprecated As of version 1.88.0 */
if (this.isLegacySyntax()) {
oNode = this.oData;
}
if (oContext instanceof Context) {
oNode = this._getObject(oContext.getPath());
} else if (oContext != null) {
oNode = oContext;
}
if (!sPath) {
return oNode;
}
var aParts = sPath.split("/"),
iIndex = 0;
if (!aParts[0]) {
// absolute path starting with slash
oNode = this.oData;
iIndex++;
}
while (oNode && aParts[iIndex]) {
oNode = oNode[aParts[iIndex]];
iIndex++;
}
return oNode;
};
JSONModel.prototype.isList = function(sPath, oContext) {
var sAbsolutePath = this.resolve(sPath, oContext);
return Array.isArray(this._getObject(sAbsolutePath));
};
/**
* Sets the meta model associated with this model
*
* @private
* @param {sap.ui.model.MetaModel} oMetaModel the meta model associated with this model
*/
JSONModel.prototype._setMetaModel = function(oMetaModel) {
this._oMetaModel = oMetaModel;
};
JSONModel.prototype.getMetaModel = function() {
return this._oMetaModel;
};
/**
* Returns the value of the property for the given <code>sPath</code> and <code>oContext</code>.
*
* @param {string} sPath The path to the object you want to read
* @param {sap.ui.model.Context} [oContext] The context that resolves the path
*
* @returns {any|null|undefined}
* The value for the given <code>sPath</code> and <code>oContext</code>.
* If the property path derived from the given <code>sPath</code> and <code>oContext</code> is
* absolute (starts with a "/") but does not lead to a property in the data structure,
* <code>undefined</code> is returned. If the property <code>sPath</code> is not absolute, <code>null</code>
* is returned.
*
* @function
* @name sap.ui.model.json.JSONModel.getObject
* @deprecated As of version 1.145.0, use {@link #getProperty} instead
* @public
*/
/**
* Returns the original value for the property with the given <code>sPath</code> and <code>oContext</code>.
*
* @param {string} sPath The path/name of the property
* @param {sap.ui.model.Context} [oContext] Context for accessing the property value
*
* @returns {any|null|undefined}
* The value of the property for the given <code>sPath</code> and <code>oContext</code>.
* If the property path derived from the given <code>sPath</code> and <code>oContext</code> is
* absolute (starts with a "/") but does not lead to a property in the data structure,
* <code>undefined</code> is returned. If the property <code>sPath</code> is not absolute, <code>null</code>
* is returned.
* @function
* @name sap.ui.model.json.JSONModel.getOriginalProperty
* @deprecated As of version 1.145.0, use {@link #getProperty} instead
* @public
*/
return JSONModel;
});