UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

353 lines (313 loc) 14.6 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides module sap.ui.core.mvc.EventHandlerResolver. sap.ui.define([ "sap/ui/base/BindingParser", "sap/ui/core/CommandExecution", "sap/ui/model/BindingMode", "sap/ui/model/CompositeBinding", "sap/ui/model/json/JSONModel", "sap/ui/model/base/ManagedObjectModel", "sap/base/util/JSTokenizer", "sap/base/util/resolveReference", "sap/base/future", "sap/ui/base/DesignTime" ], function( BindingParser, CommandExecution, BindingMode, CompositeBinding, JSONModel, MOM, JSTokenizer, resolveReference, future, DesignTime ) { "use strict"; var EventHandlerResolver = { /** * Helper method to resolve an event handler either locally (from a controller) or globally. * * Which contexts are checked for the event handler depends on the syntax of the name: * <ul> * <li><i>relative</i>: names starting with a dot ('.') must specify a handler in * the controller (example: <code>".myLocalHandler"</code>)</li> * <li><i>absolute</i>: names that contain, but do not start with a dot ('.') are first checked * against the given local variables <code>mLocals</code>. When it can't be resolved, it's * then assumed to mean a global handler function. {@link module:sap/base/util/ObjectPath.get} * will be used to retrieve the function (example: <code>"some.global.handler"</code> )</li> * <li><i>legacy</i>: Names that contain no dot at all are first checked against the * <code>oController</code>. If nothing is found, it's interpreted as a relative name and * then - if nothing is found - as an absolute name. This variant is only supported for * backward compatibility (example: <code>"myHandler"</code>)</li> * </ul> * * The event handler name can either be a pure function name (defined in the controller, or globally, * as explained above), or the function name can be followed by braces containing parameters that * shall be passed to the handler instead of the event object. In case of braces the entire string is * parsed like a binding expression, so in addition to static values also bindings and certain operators * can be used. * * As long as no event handler parameters are specified and regardless of where the function * was looked up, the event handler will be executed with the given <code>oController</code> * as the context object ('this'). This should allow the implementation of generic global * handlers that might need an easy back link to the controller/view in which they are currently * used (e.g. to call createId/byId). It also makes the development of global event handlers * more consistent with controller local event handlers. However, once event parameters are specified, * the 'this' context is always the object on which the handler function is defined. * * <strong>Note</strong>: It is not mandatory but improves readability of declarative views when * legacy names are converted to relative names where appropriate. * * @param {string} sName the event handler name to resolve * @param {sap.ui.core.mvc.Controller} oController the controller to use as context * @param {object} [mLocals] local variables allowed in the sName as map of variable name to value * @return {any[]} an array with function and context object, suitable for applySettings. * @private */ resolveEventHandler: function(sName, oController, mLocals) { var fnHandler, iStartBracket, sFunctionName; sName = sName.trim(); if (DesignTime.isControllerCodeDeactivated()) { // When design mode is enabled, controller code is not loaded. That is why we stub the handler functions. fnHandler = function() {}; } else { //check for command usage - create handler that triggers the CommandExecution if (sName.startsWith("cmd:")) { var sCommand = sName.substr(4); fnHandler = function(oEvent) { var oCommandExecution = CommandExecution.find(oEvent.getSource(), sCommand); if (oCommandExecution) { oCommandExecution.trigger(); } else { future.errorThrows("Handler '" + sName + "' could not be resolved. No CommandExecution defined for command: " + sCommand); } }; fnHandler._sapui_commandName = sCommand; } else { // check for extended event handler syntax iStartBracket = sName.indexOf("("); sFunctionName = sName; if (iStartBracket > 0) { sFunctionName = sName.substring(0, iStartBracket).trim(); } else if (iStartBracket === 0) { throw new Error("Event handler name starts with a bracket, must start with a function name " + "(or with a dot followed by controller-local function name): " + sName); } fnHandler = resolveReference(sFunctionName, Object.assign({".": oController}, mLocals), { // resolve a name without leading dot under oController only when it doesn't contains dot preferDotContext: sFunctionName.indexOf(".") === -1, // the resolved function shouldn't be bound to any context because it may need to be bound // to controller regardless where the handler is resolved if the sFunctionName doesn't have // parentheses bindContext: false } ); } // handle extended event handler syntax if (fnHandler && iStartBracket > 0) { var iEndBracket = sName.lastIndexOf(")"); if (iEndBracket > iStartBracket) { if (sName.substring(iStartBracket).indexOf("{=") > -1) { future.warningThrows("It looks like an event handler parameter contains a binding expression ({=...}). This is not allowed " + "because the entire event handler is already considered an expression: " + sName); } // create a new handler function as wrapper that internally provides the configured values as arguments to the actual handler function fnHandler = (function(sFunctionName, oController) { // the previous fnHandler is not used because the expression parser resolves and calls the original handler function return function(oEvent) { // the actual event handler function; soon enriched with more context, then calling the configured fnHandler var oParametersModel, oSourceModel, sExpression = sName; // the expression to actually pass to the parser; // configure the resolver element with additional models if (sName.indexOf("$parameters") > -1) { oParametersModel = new JSONModel(oEvent.mParameters); } if (sName.indexOf("$source") > -1) { oSourceModel = new MOM(oEvent.getSource()); } var mEventHandlerVariables = {"$controller": oController, $event: oEvent}; if (sFunctionName.indexOf(".") > 0) { // if function has no leading dot (which would mean it is a Controller method), but has a dot later on, accept the first component as global object var sGlobal = sFunctionName.split(".")[0]; mEventHandlerVariables[sGlobal] = window[sGlobal]; } else if (sFunctionName.indexOf(".") === -1) { if (oController && oController[sFunctionName]) { // if function has no dot at all, and oController has a member with the same name, this member should be used as function // (this tells the expression parser to use the same logic as applied above) sExpression = "$controller." + sExpression; } else if (window[sFunctionName]) { mEventHandlerVariables[sFunctionName] = window[sFunctionName]; } } // if a scope object exist, assign the scope object to the global object Object.assign(mEventHandlerVariables, mLocals); // the following line evaluates the expression // in case all parameters are constants, it already calls the event handler along with all its arguments, otherwise it returns a binding info var oExpressionParserResult = BindingParser.parseExpression(sExpression.replace(/^\./, "$controller."), 0, {oContext: oController}, mEventHandlerVariables); if (oExpressionParserResult.result) { // a binding info // we need to trigger evaluation (but we don't need the result, evaluation already calls the event handler) try { getBindingValue(oExpressionParserResult.result, oEvent.getSource(), oController, oParametersModel, oSourceModel); } catch (e) { e.message = "Error when evaluating event handler '" + sName + "': " + e.message; throw e; } } if (oParametersModel) { oParametersModel.destroy(); } if (oSourceModel) { oSourceModel.destroy(); } }; })(sFunctionName, oController); } else { future.errorThrows("Syntax error in event handler '" + sName + "': arguments must be enclosed in a pair of brackets"); } } } if ( typeof fnHandler === "function" ) { // the original handler definition is set as property of the resulting function to keep this information // e.g. for serializers which convert a control tree back to a serialized format fnHandler._sapui_handlerName = sName; // always attach the handler with the controller as context ('this') return [ fnHandler, oController ]; } future.warningThrows("Event handler name '" + sName + "' could not be resolved to an event handler function"); // return undefined }, /** * Parses and splits the incoming string into meaningful event handler definitions * * Examples: * * parse(".fnControllerMethod") * => [".fnControllerMethod"] * * parse(".doSomething('Hello World'); .doSomething2('string'); globalFunction") * => [".doSomething('Hello World')", ".doSomething2('string')", "globalFunction"] * parse(".fnControllerMethod; .fnControllerMethod(${ path:'/someModelProperty', formatter: '.myFormatter', type: 'sap.ui.model.type.String'} ); globalFunction") * => [".fnControllerMethod", ".fnControllerMethod(${ path:'/someModelProperty', formatter: '.myFormatter', type: 'sap.ui.model.type.String'} )", "globalFunction"] * * @param {string} [sValue] - Incoming string * @return {string[]} - Array of event handler definitions */ parse: function parse(sValue) { sValue = sValue.trim(); var oTokenizer = new JSTokenizer(); var aResult = []; var sBuffer = ""; var iParenthesesCounter = 0; oTokenizer.init(sValue, 0); for (;;) { var sSymbol = oTokenizer.next(); if ( sSymbol === '"' || sSymbol === "'" ) { var pos = oTokenizer.getIndex(); oTokenizer.string(); sBuffer += sValue.slice(pos, oTokenizer.getIndex()); sSymbol = oTokenizer.getCh(); } if ( !sSymbol ) { break; } switch (sSymbol) { case "(": iParenthesesCounter++; break; case ")": iParenthesesCounter--; break; default: break; } if (sSymbol === ";" && iParenthesesCounter === 0) { aResult.push(sBuffer.trim()); sBuffer = ""; } else { sBuffer += sSymbol; } } if (sBuffer) { aResult.push(sBuffer.trim()); } return aResult; } }; function getBindingValue(oBindingInfo, oElement, oController, oParametersModel, oSourceModel) { // TODO: refactor ManagedObject and re-use parts that have been copied here var oType, oPart; oBindingInfo.mode = BindingMode.OneWay; if (!oBindingInfo.parts) { oBindingInfo.parts = []; oBindingInfo.parts[0] = { path: oBindingInfo.path, targetType: oBindingInfo.targetType, type: oBindingInfo.type, suspended: oBindingInfo.suspended, formatOptions: oBindingInfo.formatOptions, constraints: oBindingInfo.constraints, model: oBindingInfo.model, mode: oBindingInfo.mode }; delete oBindingInfo.path; delete oBindingInfo.targetType; delete oBindingInfo.mode; delete oBindingInfo.model; } for (var i = 0; i < oBindingInfo.parts.length; i++) { oPart = oBindingInfo.parts[i]; if (typeof oPart == "string") { oPart = { path: oPart }; oBindingInfo.parts[i] = oPart; } if (!oPart.path && oPart.parts) { throw new Error("Bindings in event handler parameters cannot use parts. Just use one single path."); } // if a model separator is found in the path, extract model name and path var iSeparatorPos = oPart.path.indexOf(">"); if (iSeparatorPos > 0) { oPart.model = oPart.path.substr(0, iSeparatorPos); oPart.path = oPart.path.substr(iSeparatorPos + 1); } } var oContext, oBinding, aBindings = []; oBindingInfo.parts.forEach(function(oPart) { var oModel; if (oPart.model === "$parameters") { oModel = oParametersModel; oContext = oParametersModel.createBindingContext("/"); } else if (oPart.model === "$source") { oModel = oSourceModel; oContext = oSourceModel.createBindingContext("/"); } else { oModel = oElement.getModel(oPart.model); oContext = oElement.getBindingContext(oPart.model); } oType = oPart.type; oBinding = oModel.bindProperty(oPart.path, oContext, oBindingInfo.parameters); oBinding.setType(oType /* type that is able to parse etc. */, oPart.targetType /* string, boolean, etc. */ || "any"); oBinding.setFormatter(oPart.formatter); oBinding.setBindingMode(BindingMode.OneTime); aBindings.push(oBinding); }); // check if we have a composite binding or a formatter function created by the BindingParser which has property textFragments if (aBindings.length > 1 || ( oBindingInfo.formatter && oBindingInfo.formatter.textFragments )) { // Create type instance if needed oType = oBindingInfo.type; oBinding = new CompositeBinding(aBindings, oBindingInfo.useRawValues, oBindingInfo.useInternalValues); oBinding.setType(oType /* type that is able to parse etc. */, oPart.targetType /* string, boolean, etc. */ || "any"); oBinding.setBindingMode(BindingMode.OneTime); } else { oBinding = aBindings[0]; } oBinding.setFormatter(oBindingInfo.formatter); // this context is overridden by view parser (it assigns the controller) oBinding.initialize(); return oBinding.getExternalValue(); } return EventHandlerResolver; } );