Skip to content

Commit

Permalink
[FEATURE] mvc/XMLView: support core:require tag
Browse files Browse the repository at this point in the history
With this change it is possible to require Modules in XMLViews and
use their static functions as event handlers, formatters or inside
of ExpressionBindings. That can help to avoid global variables or
eases reuse of certain helper classes without indirection via the
Controller.

A new private module resolveReference has been added to unify the 
coding over the three modules BindingParser, DataType and the
EventHandlerResolver.

JIRA: CPOUIFPHOENIXCORE-2660
Change-Id: I13a2fe4643b08373162184c6bffd5e086027f5e9
  • Loading branch information
stopcoder authored and codeworrior committed Jul 23, 2019
1 parent 260da4c commit 2dab48e
Show file tree
Hide file tree
Showing 38 changed files with 1,993 additions and 262 deletions.
149 changes: 149 additions & 0 deletions src/sap.ui.core/src/sap/base/util/resolveReference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*!
* ${copyright}
*/
sap.ui.define(["sap/base/util/ObjectPath"], function(ObjectPath) {
"use strict";

// indicator if the reference can't be resolved
var oNotFound = Object.create(null);

/**
* Resolve the path segments under the given root context
*
* @param {array} aParts The path segments
* @param {object} oRoot The root context
* @param {object} [mOptions] Options
* @param {boolean} [mOptions.bindContext] When the resolved value is a
* function, whether the resolved function is bound to a context
* @param {boolean} [mOptions.rootContext] When the resolved value is a
* function and a rootContext is given, the resolved function is bound
* to this context instead of the object to which it belongs. If
* <code>mOptions.bindContext=false</code>, this option has no effect
* @return {any} The resolved value. If the value can't be resolved under the
* given root context, it returns <code>oNotFound</code>.
*/
function _resolve(aParts, oRoot, mOptions) {
var vRef, oContext;

if (oRoot && (aParts[0] in oRoot)) {
// the path consists of at least two segments
// e.g. key "Module.namespace.function" -> function() {...}
oContext = aParts.length > 1 ? ObjectPath.get(aParts.slice(0, -1), oRoot) : oRoot;
vRef = oContext && oContext[aParts[aParts.length - 1]];

if (typeof vRef === "function" && mOptions.bindContext) {
vRef = vRef.bind(mOptions.rootContext || oContext);
}

return vRef;
}

return oNotFound;
}

/**
* Returns a value located in the provided path using the given
* <code>mVariables</code> object.
*
* If the provided path cannot be resolved completely, <code>undefined</code> is returned.
*
* How <code>mVariables</code> are checked for resolving the path depends on
* the syntax of the path:
* <ul>
* <li><i>absolute</i>: paths not starting with a dot ('.') are checked through
* <code>mVariables</code>.</li>
* <li><i>relative</i>: paths starting with a dot ('.') are only checked through the
* dot variable <code>mVariables["."]</code> and don't fallback to global scope
* <code>window</code>.</li>
* <li><i>legacy</i>: when <code>mOptions.preferDotContext=true</code>, paths not starting
* with a dot ('.') are first checked through the dot Variable
* <code>mVariables["."]</code> and then - if nothing is found - through the other
* Variables in <code>mVariables</code> and eventually fallback to global scope
* <code>window</code>.</li>
* </ul>
*
* When the resolved value is a function, a context may be bound to it with the following
* conditions:
* <ul>
* <li><i>No bound</i>: if the function is resolved from the global scope (not from any
* given variables in <code>mVariables</code>, it's not bound to any context. If the
* function exists directly under <code>mVariables</code>, nothing is bound.</li>
* <li><i>Bound</i>: otherwise, the resolved function is bound to the object to which it
* belongs</li>
* <li><i>mOptions.bindContext</i>: when this option is set to <code>false</code>, no
* context is bound to the resolved function regardless where the function is resolved
* </li>
* <li><i>mOptions.bindDotContext</i>: for paths starting with a dot ('.'),
* <code>mOptions.bindDotContext=false</code> turns off the automatic binding to the
* dot variable <code>mVariables["."]</code>. <code>mOptions.bindDotContext</code> has
* no effect when <code>mOptions.bindContext=false</code>.</li>
* </ul>
*
* @function
* @private
* @ui5-restricted sap.ui.core
* @since 1.69
*
* @param {string} sPath Path
* @param {object} [mVariables] An object containing the mapping of variable name to object or function
* @param {object} [mOptions] Options
* @param {boolean} [mOptions.preferDotContext=false] Whether the path not starting with a dot ('.') is
* resolved under the dot variable when it can not be resolved through the given variables object.
* @param {boolean} [mOptions.bindContext=true] When the resolved value is a function, whether the
* resolved function is bound to a context. When this property is set to false, the
* mOptions.bindDotContext has no effect anymore.
* @param {boolean} [mOptions.bindDotContext=true] When the resolved value is a function, whether the
* resolved function from a path which starts with a dot ('.') should be bound to the dot context
* @returns {any} Returns the value located in the provided path, or <code>undefined</code> if the path
* does not exist completely.
* @alias module:sap/base/util/resolveReference
*/
var resolveReference = function(sPath, mVariables, mOptions) {
// fill the default values
mVariables = mVariables || {};
mOptions = mOptions || {};
mOptions.bindContext = mOptions.bindContext !== false;
mOptions.bindDotContext = mOptions.bindDotContext !== false;

var aParts = sPath.split("."),
// if sPath starts with ".", split returns an empty string
// at the first position and the dot is used as variable
sVariable = aParts.shift() || ".",
bDotCase = sVariable === ".",
vRef = oNotFound;

// push the first part back to the array
aParts.unshift(sVariable);

// if preferDotContext, resolve the sPath under the dot context first for sPath which doesn't begin with "."
if (mOptions.preferDotContext && !bDotCase) {
vRef = _resolve(aParts, mVariables["."], {
bindContext: mOptions.bindContext && mOptions.bindDotContext,
// resolve function in dot variable should always bind the dot variable
rootContext: mVariables["."]
});
}

// If no value is returned, resolve the path under mVariables
if (!vRef || vRef === oNotFound) {
vRef = _resolve(aParts, mVariables, {
bindContext: mOptions.bindContext
// dot case: mOptions.bindDotContext determins whether context should be bound
// non dot case: bind context if sPath contains more than one segment
&& (bDotCase ? mOptions.bindDotContext : (aParts.length > 1)),
rootContext: bDotCase ? mVariables["."] : undefined
});
}

// resolve the path under global scope, only when it can't be resolved under mVariables
if (vRef === oNotFound) {
// fallback if no value could be found under the given sPath's first segment
// otherwise resolve under global namespace
vRef = ObjectPath.get(sPath);
}

return vRef;
};

return resolveReference;
});
65 changes: 38 additions & 27 deletions src/sap.ui.core/src/sap/ui/base/BindingParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ sap.ui.define([
'sap/ui/model/Sorter',
"sap/base/Log",
'sap/base/util/ObjectPath',
"sap/base/util/JSTokenizer"
"sap/base/util/JSTokenizer",
"sap/base/util/resolveReference"
], function(
ExpressionParser,
BindingMode,
Filter,
Sorter,
Log,
ObjectPath,
JSTokenizer
JSTokenizer,
resolveReference
) {
"use strict";

Expand Down Expand Up @@ -156,6 +158,7 @@ sap.ui.define([
}

function resolveBindingInfo(oEnv, oBindingInfo) {
var mVariables = Object.assign({".": oEnv.oContext}, oEnv.mLocals);

/*
* Resolves a function name to a function.
Expand All @@ -164,20 +167,21 @@ sap.ui.define([
*
* If the name starts with a dot ('.'), lookup happens within the given context only;
* otherwise it will first happen within the given context (only if
* <code>bPreferContext</code> is set) and then fall back to the global context (window).
* <code>bPreferContext</code> is set) and then use <code>mLocals</code> to resolve
* the function and finally fall back to the global context (window).
*
* @param {object} o Object from which the property should be read and resolved
* @param {string} sProp name of the property to resolve
*/
function resolveRef(o,sProp) {
if ( typeof o[sProp] === "string" ) {
var fn, sName = o[sProp];
if ( o[sProp][0] === "." ) {
fn = ObjectPath.get(o[sProp].slice(1), oEnv.oContext);
o[sProp] = oEnv.bStaticContext ? fn : (fn && fn.bind(oEnv.oContext));
} else {
o[sProp] = oEnv.bPreferContext && ObjectPath.get(o[sProp], oEnv.oContext) || ObjectPath.get(o[sProp]);
}
var sName = o[sProp];

o[sProp] = resolveReference(o[sProp], mVariables, {
preferDotContext: oEnv.bPreferContext,
bindDotContext: !oEnv.bStaticContext
});

if (typeof (o[sProp]) !== "function") {
if (oEnv.bTolerateFunctionsNotFound) {
oEnv.aFunctionsNotFound = oEnv.aFunctionsNotFound || [];
Expand All @@ -192,8 +196,8 @@ sap.ui.define([
/*
* Resolves a data type name and configuration either to a type constructor or to a type instance.
*
* The name is resolved locally (against oEnv) if it starts with a '.', otherwise against
* the global context (window).
* The name is resolved locally (against oEnv.oContext) if it starts with a '.', otherwise against
* the oEnv.mLocals and if it's still not resolved, against the global context (window).
*
* The resolution is done inplace. If the name resolves to a function, it is assumed to be the
* constructor of a data type. A new instance will be created, using the values of the
Expand All @@ -205,11 +209,10 @@ sap.ui.define([
function resolveType(o) {
var FNType;
if (typeof o.type === "string" ) {
if ( o.type[0] === "." ) {
FNType = ObjectPath.get(o.type.slice(1), oEnv.oContext);
} else {
FNType = ObjectPath.get(o.type);
}
FNType = resolveReference(o.type, mVariables, {
bindContext: false
});

// TODO find another solution for the type parameters?
if (typeof FNType === "function") {
o.type = new FNType(o.formatOptions, o.constraints);
Expand Down Expand Up @@ -378,19 +381,22 @@ sap.ui.define([
* string array <code>functionsNotFound</code> of the result object; else they are logged
* as errors
* @param {boolean} [bStaticContext=false]
* if true, relative function names found via <code>oContext</code> will not be treated as
* instance methods of the context, but as static methods
* If true, relative function names found via <code>oContext</code> will not be treated as
* instance methods of the context, but as static methods.
* @param {boolean} [bPreferContext=false]
* if true, names without an initial dot are searched in the given context first and then
* globally
* @param {object} [mLocals]
* variables allowed in the expression as map of variable name to its value
*/
BindingParser.complexParser = function(sString, oContext, bUnescape,
bTolerateFunctionsNotFound, bStaticContext, bPreferContext) {
bTolerateFunctionsNotFound, bStaticContext, bPreferContext, mLocals) {
var b2ndLevelMergedNeeded = false, // whether some 2nd level parts again have parts
oBindingInfo = {parts:[]},
bMergeNeeded = false, // whether some top-level parts again have parts
oEnv = {
oContext: oContext,
mLocals: mLocals,
aFunctionsNotFound: undefined, // lazy creation
bPreferContext : bPreferContext,
bStaticContext: bStaticContext,
Expand All @@ -414,7 +420,7 @@ sap.ui.define([
*/
function expression(sInput, iStart, oBindingMode) {
var oBinding = ExpressionParser.parse(resolveEmbeddedBinding.bind(null, oEnv), sString,
iStart, null, bStaticContext ? oContext : null);
iStart, null, mLocals || (bStaticContext ? oContext : null));

/**
* Recursively sets the mode <code>oBindingMode</code> on the given binding (or its
Expand Down Expand Up @@ -620,8 +626,8 @@ sap.ui.define([
* the index to start parsing
* @param {object} [oEnv]
* the "environment" (see resolveEmbeddedBinding function for details)
* @param {object} [mGlobals]
* global variables allowed in the expression as map of variable name to its value
* @param {object} [mLocals]
* variables allowed in the expression as map of variable name to value
* @returns {object}
* the parse result with the following properties
* <ul>
Expand All @@ -636,11 +642,16 @@ sap.ui.define([
* the error contains the position where parsing failed.
* @private
*/
BindingParser.parseExpression = function (sInput, iStart, oEnv, mGlobals) {
return ExpressionParser.parse(resolveEmbeddedBinding.bind(null, oEnv || {}), sInput, iStart,
mGlobals);
BindingParser.parseExpression = function (sInput, iStart, oEnv, mLocals) {
oEnv = oEnv || {};

if (mLocals) {
oEnv.mLocals = mLocals;
}

return ExpressionParser.parse(resolveEmbeddedBinding.bind(null, oEnv), sInput, iStart, mLocals);
};

return BindingParser;

}, /* bExport= */ true);
}, /* bExport= */ true);
29 changes: 11 additions & 18 deletions src/sap.ui.core/src/sap/ui/base/DataType.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ sap.ui.define([
'sap/base/util/ObjectPath',
"sap/base/assert",
"sap/base/Log",
"sap/base/util/isPlainObject"
"sap/base/util/isPlainObject",
'sap/base/util/resolveReference'
],
function(ObjectPath, assert, Log, isPlainObject) {
function(ObjectPath, assert, Log, isPlainObject, resolveReference) {
"use strict";


Expand Down Expand Up @@ -447,24 +448,15 @@ sap.ui.define([
"simple identifiers (A-Z, 0-9, _ or $) only, but was '" + sValue + "'");
}

// TODO implementation should be moved to / shared with EventHandlerResolver
var fnResult,
bLocal,
contextObj = _oOptions && _oOptions.context;

if ( sValue[0] === '.' ) {
// starts with a dot, must be a controller local function
// usage of ObjectPath.get to allow addressing functions in properties
if ( contextObj ) {
fnResult = ObjectPath.get(sValue.slice(1), contextObj);
bLocal = true;
}
} else {
fnResult = ObjectPath.get(sValue);
}
oContext = _oOptions && _oOptions.context,
oLocals = _oOptions && _oOptions.locals;

fnResult = resolveReference(sValue,
Object.assign({".": oContext}, oLocals));

if ( fnResult && this.isValid(fnResult) ) {
return bLocal ? fnResult.bind(contextObj) : fnResult;
return fnResult;
}

throw new TypeError("The string '" + sValue + "' couldn't be resolved to a function");
Expand Down Expand Up @@ -649,6 +641,7 @@ sap.ui.define([
return mInterfaces.hasOwnProperty(sType) && ObjectPath.get(sType) === mInterfaces[sType];
};


return DataType;

}, /* bExport= */ true);
}, /* bExport= */ true);
Loading

0 comments on commit 2dab48e

Please sign in to comment.