@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
632 lines (529 loc) • 19.5 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
'./Template',
'./TemplateControl',
'sap/ui/thirdparty/handlebars',
'sap/ui/base/ManagedObject',
'sap/base/util/ObjectPath',
"sap/base/security/encodeXML",
"sap/base/util/isEmptyObject",
'sap/ui/core/Core' // provides sap.ui.getCore()
],
function(
Template,
TemplateControl,
Handlebars,
ManagedObject,
ObjectPath,
encodeXML,
isEmptyObject
) {
"use strict";
/**
* Creates and initializes a new handlebars template with the given <code>sId</code>
* and settings.
*
* The set of allowed entries in the <code>mSettings</code> object depends on
* the concrete subclass and is described there.
*
* @param {string}
* [sId] optional id for the new template; generated automatically if
* no non-empty id is given Note: this can be omitted, no matter
* whether <code>mSettings</code> will be given or not!
* @param {object}
* [mSettings] optional map/JSON-object with initial settings for the
* new component instance
* @public
*
* @class The class for Handlebars Templates.
* @extends sap.ui.core.tmpl.Template
* @abstract
* @author SAP SE
* @version 1.111.5
* @alias sap.ui.core.tmpl.HandlebarsTemplate
* @since 1.15
* @deprecated since 1.56
*/
var HandlebarsTemplate = Template.extend("sap.ui.core.tmpl.HandlebarsTemplate", /** @lends sap.ui.core.tmpl.HandlebarsTemplate.prototype */
{
metadata: {
library: "sap.ui.core"
},
constructor : function(sId, mSettings) {
Template.apply(this, arguments);
}
});
// register this template type (as it is the default we do it also in the Template)
Template.registerType("text/x-handlebars-template", "sap.ui.core.tmpl.HandlebarsTemplate");
function decodeHash(oHash) {
for (var sKey in oHash) {
oHash[sKey] = oHash[sKey].replace(">", ">").replace("<", "<").replace(""", "\"").replace("&", "&");
}
}
function determinePath(sPath, sParentPath) {
// split given paths in model name (optional) and path
var rPath = /^((\w+)>)?(.*)/,
aPathParts = rPath.exec(sPath),
sPathModel = aPathParts[2],
aParentPathParts = rPath.exec(sParentPath),
sParentPathModel = aParentPathParts[2];
var aPathParts = rPath.exec(sPath);
// if the model matches and the path is not absolute they need to be concatenated
if (sParentPath && sPathModel == sParentPathModel) {
return sParentPath + aPathParts[3];
} else {
return sPath;
}
}
/**
* Handlebars helpers for the rendering phase!
* @private
*/
HandlebarsTemplate.RENDER_HELPERS = (function() {
// TODO: find a point in time for destroying this RenderManager again
// extended helpers:
// - each
// - if
// - unless
// - with (TODO)
// custom helpers:
// - control: allows to declare a UI5 control
// - element
// - attribute
// - property
// - aggregation
// define the options to render the properties, aggregations, events, ...
var fnEach = Handlebars.helpers["each"],
fnWith = Handlebars.helpers["with"],
fnIf = Handlebars.helpers["if"],
fnUnless = Handlebars.helpers["unless"],
oRenderManager = sap.ui.getCore().createRenderManager();
// this special RenderManager is used to write the controlData, classes
// and styles into the buffer and extract it later on via getHTML!
oRenderManager.renderControl = function(oControl) {
this.writeControlData(oControl);
this.writeClasses(oControl);
this.writeStyles(oControl);
};
var oHelpers = {
"each": function(context, options) {
options = options || context;
if (!options.hash.path) {
// call the original function
return fnEach.apply(this, arguments);
} else {
// make sure to decode the hash entries
decodeHash(options.hash);
// parse the path & find the model
var oRM = options.data.renderManager,
oRootControl = options.data.rootControl,
sParentPath = options.data.path,
oParentControl = options.data.parentControl,
sPath = determinePath(options.hash.path, sParentPath),
oProperty = oRootControl.bindList(sPath),
aHTML = [],
data;
// frame the data (isolation)
if (options.data) {
data = Handlebars.createFrame(options.data);
}
// iterate through the entries of the property
if (oProperty) {
for (var sKey in oProperty) {
//var oValue = oProperty[sKey];
if (data) {
data.renderManager = oRM;
data.rootControl = oRootControl;
data.path = sPath + "/" + sKey + "/";
data.parentControl = oParentControl;
}
// pass the current value as context to allow nested
// usage of the standard placeholders
//aHTML.push(options.fn(oValue, { data: data }));
// we do not pass a context since the expressions for UI5 are
// based on the models
aHTML.push(options.fn({}, { data: data }));
}
}
// let's return the markup
if (!oParentControl) {
return new Handlebars.SafeString(aHTML.join(""));
}
}
},
"with": function(context, options) {
options = options || context;
if (!options.hash.path) {
// call the original function
return fnWith.apply(this, arguments);
}
},
"if": function(conditional, options) {
options = options || conditional;
if (!options.hash.path) {
// call the original function
return fnIf.apply(this, arguments);
} else {
// make sure to decode the hash entries
decodeHash(options.hash);
// lookup the required infos
var oRootControl = options.data.rootControl,
sParentPath = options.data.path,
sPath = determinePath(options.hash.path, sParentPath);
// only in case of a path is specified the handler can work
if (sPath) {
// bind and returns true/false dependent on the value
var oValue = oRootControl.bindProp(sPath);
if (oValue) {
return options.fn(this);
} else {
return options.inverse(this);
}
}
}
},
"unless": function(conditional, options) {
options = options || conditional;
if (!options.hash.path) {
// call the original function
return fnUnless.apply(this, arguments);
} else {
// make sure to decode the hash entries
decodeHash(options.hash);
// lookup the required infos
var oRootControl = options.data.rootControl,
sParentPath = options.data.path,
sPath = determinePath(options.hash.path, sParentPath);
// only in case of a path is specified the handler can work
if (sPath) {
// bind and returns true/false dependent on the value
var oValue = oRootControl.bindProp(sPath);
if (!oValue) {
return options.fn(this);
} else {
return options.inverse(this);
}
}
}
},
"text": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// lookup the required infos
var oRootControl = options.data.rootControl,
sParentPath = options.data.path,
sPath = determinePath(options.hash.path, sParentPath);
// only in case of a path is specified the handler can work
if (sPath) {
// bind and return the text
var oValue = oRootControl.bindProp(sPath);
return oValue && new Handlebars.SafeString(encodeXML(oValue));
} else {
throw new Error("The expression \"text\" requires the option \"path\"!");
}
},
"element": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// create and return the DOM element
var oRM = options.data.renderManager,
oRootControl = options.data.rootControl,
oElement = oRootControl.createDOMElement(options.hash, options.data.path),
oParentElement = options.data.parentElement;
// Example for defining nested elements:
// {{#element ...}}
// {{element ...}} <-- nested element
// {{/element}}
if (options.fn) {
options.fn({}, {
data: {
renderManager: oRM,
rootControl: oRootControl,
parentElement: oElement
}
});
}
// in case of having a parent DOM element, we add the DOM element to
// the parent and do not return an HTML string
if (oParentElement) {
oParentElement.addElement(oElement);
return;
}
// create the HTML markup and return it
return new Handlebars.SafeString(oRM.getHTML(oElement));
},
"control": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// extract the data information
var oRM = options.data.renderManager,
oControl = options.data.control;
// aggregation support to render the control only (to support markup)
// e.g.:
// {{#aggregation name="content" type="sap.ui.core.Control" multiple="true"}}
// <div>
// {{control}} <== place where to render the control!
// </div>
// {{/aggregation}}
if (oControl) {
return new Handlebars.SafeString(oRM.getHTML(oControl));
}
// extract the rest of the information which is required now
var oRootControl = options.data.rootControl,
sParentPath = options.data.path,
mParentChildren = options.data.children,
sType = options.hash["sap-ui-type"],
oClass = ObjectPath.get(sType || ""),
oMetadata = oClass && oClass.getMetadata(),
sDefaultAggregation = options.hash["sap-ui-default-aggregation"] || oMetadata && oMetadata.getDefaultAggregationName(),
oView = options.data.view;
// throw error if control cannot be found
if (!oClass) {
throw new Error("Control of type " + sType + " cannot be found.");
}
// Nested controls will get the reference to the parent control in order
// to add them to the defined aggregation. Example of nested controls:
// {{#control ...}}
// {{control ...}} <-- nested control
// {{/control}}
var mChildren = {};
if (options.fn) {
options.fn({}, {
data: {
rootControl: oRootControl,
path: sParentPath,
children: mChildren,
defaultAggregation: sDefaultAggregation,
view: oView
}
});
}
// remove the found nested children from the mSettings because they will
// be handled after the creation of the new control instance
var mSettings = Object.assign({}, options.hash),
aStyleClasses;
for (var sKey in mSettings) {
//var oValue = mSettings[sKey];
if (sKey === "sap-ui-class" && typeof mSettings[sKey] === "string") {
aStyleClasses = mSettings["sap-ui-class"] && mSettings["sap-ui-class"].split(" ");
delete mSettings[sKey];
} else if (mChildren[sKey]) {
delete mSettings[sKey];
}
}
// create the new control (out of the hash information)
var oNewControl = oRootControl.createControl(mSettings, options.data.path, !!mParentChildren, oView);
if (aStyleClasses && aStyleClasses.length > 0) {
aStyleClasses.forEach(oNewControl.addStyleClass.bind(oNewControl));
}
// add the created children to current control instance either as template
// in case of a binding has been found or as aggregation in case of no
// binding was found
if (!isEmptyObject(mChildren)) {
mSettings = options.hash;
var oAllAggregation = oMetadata.getAllAggregations();
for (var sAggregationName in mChildren) {
var aChildAggregation = mChildren[sAggregationName];
for (var i = 0, l = aChildAggregation.length; i < l; i++) {
var oChildControl = aChildAggregation[i],
oAggregation = oAllAggregation[sAggregationName],
bMultiple = oAggregation && oAggregation.multiple;
if (typeof mSettings[sAggregationName] === "string") {
// the aggregation is bound => so we create a binding info object
// which is used in the createControl function of the TemplateControl
// to create a proper binding
var oBindingInfo = ManagedObject.bindingParser(mSettings[sAggregationName], oView && oView.getController());
oBindingInfo.template = oChildControl;
oNewControl.bindAggregation(sAggregationName, oBindingInfo);
} else {
// the aggregation is not bound => so we add nested controls to the aggregation
if (bMultiple) {
oNewControl.addAggregation(sAggregationName, oChildControl);
} else {
oNewControl.setAggregation(sAggregationName, oChildControl);
}
}
}
}
}
// if we find a parent children map the control will not return
// the markup - furthermore the control will be added to the parent
// control in the section above into the desired aggregation
if (mParentChildren) {
var sAggregationName = options.hash["sap-ui-aggregation"] || options.data.defaultAggregation;
mParentChildren[sAggregationName] = mParentChildren[sAggregationName] || [];
mParentChildren[sAggregationName].push(oNewControl);
return;
}
// in case of the root control we return the markup
return new Handlebars.SafeString(oRM.getHTML(oNewControl));
},
"property": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// use the getter to access the property
var oRootControl = options.data.rootControl,
oMetadata = oRootControl.getMetadata(),
sPropertyName = options.hash.name,
sGetter = oMetadata.getProperty(sPropertyName)._sGetter;
return oRootControl[sGetter]();
},
"aggregation": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// when data provides the children object we are running in
// the use case to be used as kind of scope for the aggregation
// to be used and when running without children object the
// aggregation helper is used for defining an aggregation
// of a new control type being declared as template
if (options.data.children) {
// extract the required info
var sAggregationName = options.hash.name;
// By defining the aggregation helper the default aggregation
// of nested controls can be adopted
// {{#aggregation ...}}
// {{control ...}} <-- nested control
// {{/aggregation}}
if (options.fn) {
var oData = Object.assign({}, options.data, {
defaultAggregation: sAggregationName
});
options.fn({}, {
data: oData
});
}
} else {
// extract the required info
var oRM = options.data.renderManager,
oRootControl = options.data.rootControl,
oMetadata = oRootControl.getMetadata(),
sAggregationName = options.hash.name,
sGetter = oMetadata.getAggregation(sAggregationName)._sGetter,
aHTML = [];
// retrieve the child elements via the specific getter
// and create the markup for the nested elements
var aChildren = oRootControl[sGetter]();
if (aChildren) {
for (var i = 0, l = aChildren.length; i < l; i++) {
// if the aggregation contains nested content => execute it!
if (options.fn) {
aHTML.push(options.fn({}, {
data: {
renderManager: oRM,
rootControl: oRootControl,
control: aChildren[i]
}
}));
} else {
// simply render the control
aHTML.push(oRM.getHTML(aChildren[i]));
}
}
}
// return the markup
return new Handlebars.SafeString(aHTML.join(""));
}
},
"event": function(context, options) {
options = options || context;
},
"controlData": function(context, options) {
options = options || context;
// extract the required info
var oRootControl = options.data.rootControl;
// return the markup
return new Handlebars.SafeString(oRenderManager.getHTML(oRootControl));
}
};
return oHelpers;
}());
HandlebarsTemplate.prototype.createMetadata = function() {
// compile the template
// (TODO - think about avoid to compile the template multiple times)
var sTemplate = this.getContent(),
fnTemplate = this._fnTemplate = this._fnTemplate || Handlebars.compile(sTemplate);
// identify the control metadata: properties, aggregations, ...
// the template will be executed with specific options
var oMetadata = {},
mJSONKeys = TemplateControl.getMetadata().getAllSettings(),
mPrivateAggregations = TemplateControl.getMetadata().getAllPrivateAggregations();
// the options to identify the properties, aggregations, events, ...
var oHelpers = {
"property": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// identify the property and register non standard properties (anything else than id, style, class)
var sName = options.hash.name;
if (sName && sName !== "id" && !mJSONKeys[sName]) {
oMetadata.properties = oMetadata.properties || {};
oMetadata.properties[sName] = Object.assign({}, {type: "string"}, options.hash);
} else {
throw new Error("The property name \"" + sName + "\" is reserved.");
}
},
"aggregation": function(context, options) {
options = options || context;
// make sure to decode the hash entries
decodeHash(options.hash);
// identify the aggregations and register non standard aggregations (anything else than tooltip, customData, layoutData)
var sName = options.hash.name;
if (sName && !mJSONKeys[sName] && !mPrivateAggregations[sName]) {
options.hash.multiple = options.hash.multiple == "true"; // type correction
oMetadata.aggregations = oMetadata.aggregations || {};
oMetadata.aggregations[sName] = Object.assign({}, options.hash);
} else {
throw new Error("The aggregation name \"" + sName + "\" is reserved.");
}
},
"event": function(context, options) {
options = options || context;
},
"controlData": function(context, options) {
options = options || context;
oMetadata._hasControlData = true;
}
};
// ignore the following block helper
["each", "if", "unless", "with"].forEach(function(sValue) {
oHelpers[sValue] = function() {};
});
// execute the templates with the above options
fnTemplate({}, {
helpers: oHelpers
});
// return the new control metadata
return oMetadata;
};
HandlebarsTemplate.prototype.createRenderer = function(oView) {
// compile the template
// (TODO - think about avoid to compile the template multiple times)
var sTemplate = this.getContent(),
fnTemplate = this._fnTemplate = this._fnTemplate || Handlebars.compile(sTemplate);
// create the renderer for the control
var fnRenderer = function(rm, oControl) {
// execute the template with the above options
var sHTML = fnTemplate(oControl.getContext() || {}, {
data: {
renderManager: rm,
rootControl: oControl,
view: oView
},
helpers: HandlebarsTemplate.RENDER_HELPERS
});
// write the markup
rm.write(sHTML);
};
return fnRenderer;
};
return HandlebarsTemplate;
});