Composite controls are a means to save time and effort by reusing existing controls for the implementation.
For application developers, the composite control is a black box, therefore, an application developer cannot distinguish a composite control from native (non-composite) controls. As the application developer can not distinguish the controls, the control developer can change the implementation later and avoid composition (or the other way around). For existing uses of the respective control, this change is fully compatible.
Note:
If you do not intend to re-use a control in several places, a composite control may not be your best choice. Composite controls are best suited for (massive) re-use and for a public API that shields the application developer from its inner workings. If these are not your requirements, consider to use other techniques of factoring out common parts within your application. You can, for example, simply write an XML fragment or a function returning the root of some control tree.
To create a composite control, you start with crafting its API including properties, events, aggregations, and so on as you do it for any other control. Choose either element or control as base type. The following simple example combines an input field with a button that we call “search field”. To the outside world, it offers an editable value and can fire a search event.
As any other control, you can describe composite controls via the JavaScript control definition API, see Developing Controls and the following example.
// "Control" required from "sap/ui/core/Control"
const SearchField = Control.extend("my.SearchField", {
metadata: {
properties: {
"myValue": {type: "string", bindable: true}
},
aggregations: {
"_input": {type: "sap.m.Input", multiple: false, visibility: "hidden"},
"_btn": {type: "sap.m.Button", multiple: false, visibility: "hidden"}
},
// ...
}
});
Since OpenUI5 version 1.60, properties, aggregations, and associations can be configured with hidden
visibility. In this case, no accessors, no mutators, and no API documentation will be generated for them. It is still possible to bind data, regardless of the bindable
setting. The generic methods bindProperty
/bindAggregation
and unbindProperty
/unbindAggregation
are always available.
The two aggregations with visibility set to hidden
are used to hold the inner controls. Aggregations are used to define a parent-child relationship between a parent control and its children (controls or elements). The knowledge about this relationship is, for example, relevant for the OpenUI5 core to dispatch events properly, or to cleanup the children when the parent is destroyed. Hidden aggregations are control internal and are used especially to register the inner controls within the control hierarchy without making them publicly available. Because hidden aggregations are only used internally within a composite control, they are neither cloned nor generating any typed accessors or mutators.
The control implementation, that is, its behavior, contains the code for initialization and clean-up hooks as well as glue code for properties and events.
The init
function contains the composite’s parts and stores references to them. If you want to hide the composite parts, you should not assign an ID to those parts, but rather let the framework compute the IDs automatically. This reduces the possibility that a composite’s parts are accessed from outside via the Element.getElementById
function, where Element
is required from module sap/ui/core/Element
. Only if you have to access the composite parts by ID for some reason should you concatenate the main control ID (ID of your composite instance) with a single dash (-
) and an additional ID for the part, like in the following example:
// If ID needed, within the composite control definition:
this.getId()/*composite instance ID*/ + "-input"/*part ID*/
Caution:
To avoid potential conflicts with other internal part IDs, the part ID (
input
) itself must not contain any additional dashes. A conflict can occur, for example, if your composite control defines one internal part incorrectly with the IDinput-inner
and another with the IDinput
, while theinput
part itself internally uses the suffix-inner
for its subpart.Remember:
OpenUI5 reserves the single dash (
-
) for composite controls and their parts, while a double dash (--
) is used to combine the ID of views and their contained controls, and a triple dash (---
) is used to combine component IDs and the IDs of their owned controls or views.
During the init
function, the settings of the composite only have their default values. If the application developer has provided some values to the constructor, these values will only be set later on. It is, therefore, crucial for the correct behavior of your composite control that you implement one of the synchronization mechanisms described below.
Note:
When synchronizing with inner controls by calling their
ManagedObject#applySettings
API or even recreating them entirely and usingconstructor
settings, ensure that given values are escaped usingsap/ui/base/ManagedObject.escapeSettingsValue
. For more information, see Escaping Binding Syntax. String values from application data should be safeguarded against accidental interpretation as binding expressions. If the application intends to bindmyValue
, for example,setMyValue
will be called accordingly in the below sample.
// "Input" required from "sap/m/Input"
// "Button" required from "sap/m/Button"
// "ManagedObject" required from "sap/ui/base/ManagedObject"
SearchField.prototype.init = function() {
Control.prototype.init.apply(this, arguments);
this.setAggregation("_input", this._getInput());
this.setAggregation("_btn", this._getButton());
// ...
};
SearchField.prototype._getInput = function() {
let oInput = this.getAggregation("_input");
if (!oInput && !this.isDestroyStarted()) {
oInput = new Input(this.getId() + "-input", {
value: ManagedObject.escapeSettingsValue(this.getMyValue()), // Safeguard against unintentional binding interpretation
change: (oEvent) => {
this.setProperty("myValue", oEvent.getParameter("value"), true/*no rerendering needed, change originates in HTML*/);
}
});
this.setAggregation("_input", oInput);
}
return oInput;
};
// ...
You can use the exit
function to clean up your control when it is destroyed. You do not need to destroy the inner controls manually. This is done automatically by the framework because the inner controls are kept in hidden aggregations.
/**
* Clean-up hook... Framework destroys the aggregations by default.
*/
SearchField.prototype.exit = function() {
// Other cleanup tasks not covered by framework
// ...
Control.prototype.exit.apply(this, arguments);
};
Changes to settings in the API of a composite control are usually reflected in its parts. In the following example, the myValue
property is propagated to the inner Input
part. To do so, the generated setter for that property is overwritten. Make sure that you include the proper implementation which generically sets a property inside the element base class, else you would have to override the getter also.
Note how the Input
’s change event is used to update the composite’s myValue
property. Because the change originated in the HTML input field, no rerendering is needed. This is expressed by the third parameter of the setProperty
call. This trick is applicable whenever a property change does not require a rerendering on this control level.
Note:
Changing the
Input
’svalue
does trigger a rerendering of theInput
.
/**
* Propagate value to Input.
*/
SearchField.prototype.setMyValue = function(sValue){
this.setProperty("myValue", sValue, true /*no rerendering of whole composite control needed*/);
this._getInput()?.setValue(sValue);
return this;
};
Propagating the API settings to the parts is usually not as straightforward as shown in the example above. If intercepting the changes by overriding the setters is not sufficient or too complicated, an alternative approach might be to implement a single updateAllParts
method and call it at the beginning of the renderer of the composite control or in the onBeforeRendering
hook of the control itself.
You can use markup for layouting in the renderer implementation. But at the heart of it, you simply delegate (via the render manager) to the composite parts’ renderers. This is where you really benefit from re-using other controls with non-trivial renderers. If you have chosen the updateAllParts
approach to keep the composite API settings and the settings of the parts in sync, make sure that you call updateAllParts
before the real rendering starts.
SearchFieldRenderer.render = function(oRm, oSearchField) {
// oSearchField.updateAllParts() depending on your 'sync' approach
oRm.openStart("div", oSearchField);
oRm.class("SearchField");
oRm.openEnd();
oRm.renderControl(oSearchField._getInput());
oRm.renderControl(oSearchField._getButton());
oRm.close("div");
};
Related Information