Composite controls are a means to save time and effort by reusing existing controls for the implementation. This page walks you through the building blocks: defining the API, instantiating inner controls in init, propagating property changes, and writing a renderer.
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. 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"}
},
// ...
}
});
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.
InitThe init function contains the composite’s parts and stores references to them. By default, do not assign IDs to your inner controls — let the framework compute them automatically. Inner controls without an explicit ID cannot easily be reached from the outside via Element.getElementById (Element is required from module sap/ui/core/Element), which is exactly what you want for parts that are an implementation detail. If you do need a stable ID for an inner control — for example, to reference it from CSS or accessibility attributes — concatenate the composite’s own ID with a single dash (-) and a part-specific suffix, as 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-innerand another with the IDinput, while theinputpart itself internally uses the suffix-innerfor 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. To keep the composite and its inner controls in sync, override the relevant setters as shown in the Properties section below — or, if your composite has many properties or several inner controls reacting to the same property, use the model-based approach described in Synchronizing Properties via a $this Model.
Note:
When synchronizing with inner controls by calling their
ManagedObject#applySettingsAPI or even recreating them entirely and usingconstructorsettings, 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,setMyValuewill 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);
};
Caution:
When the framework destroys an aggregation by calling
destroyAggregation(or indirectly viadestroyXYZ), the named aggregation mutators (setXYZfor a0..1aggregation,removeXYZfor a0..naggregation) are not called for the aggregated children. If your composite control implements side effects in those mutator methods, you must also implement corresponding side effects in itsdestroyXYZmethod.
Changes to a composite’s properties usually need to be reflected in its inner controls. In the example below, the myValue property is propagated to the inner Input by overriding its generated setter. Inside the override, call this.setProperty(...), so that the underlying property value is stored — this also keeps the generated getter working without a separate override.
Note how the Input’s change event is used to update the composite’s myValue property in the opposite direction. Because the change originated in the HTML input field, no re-rendering of the composite is needed; this is expressed by the third parameter of the setProperty call. The same pattern applies whenever a property change does not require a re-render at the composite level.
Note:
Changing the
Input’svaluedoes trigger a re-rendering 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.
const SearchFieldRenderer = {
apiVersion: 4,
render(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");
}
};
XMLCompositeThe imperative init and renderer described on this page replace what XMLComposite does declaratively via the .control.xml fragment. The mechanics are the same: inner controls live in aggregations, the framework destroys them when the composite is destroyed, and the renderer delegates to the inner-control renderers. What changes is where the wiring is expressed.
| `XMLComposite` \(deprecated\) | Standard Composite |
|---|---|
| Inner controls declared in `.control.xml` fragment | Inner controls created in `init` and assigned to hidden aggregations |
| Fragment is rendered automatically | Write a `renderer` that delegates to the inner-control renderers |
| Inner controls destroyed automatically | Unchanged — hidden aggregations are destroyed by the framework |
| `byId("foo")` for fragment children | Keep references on `this` directly, or compute the inner control's ID as `this.getId() + "-foo"` and look it up via `Element.getElementById(...)` — there is no `byId` method on a standard `Control`. |
Example: Before/After
A small SimpleText composite that exposes a text property bound to an inner sap.m.Text.
Before migration:
<!-- XMLComposite fragment: SimpleText.control.xml -->
<core:FragmentDefinition xmlns:m="sap.m" xmlns:core="sap.ui.core">
<m:Text text="{$this>/text}"/>
</core:FragmentDefinition>
// XMLComposite JS: SimpleText.js (deprecated)
sap.ui.define([
"sap/ui/core/XMLComposite"
], function(XMLComposite) {
"use strict";
return XMLComposite.extend("my.SimpleText", {
metadata: {
properties: {
text: { type: "string", defaultValue: "" }
}
}
});
});
After migration:
// Standard Composite: SimpleText.js
sap.ui.define([
"sap/ui/core/Control",
"sap/m/Text"
], function(Control, Text) {
"use strict";
const SimpleText = Control.extend("my.SimpleText", {
metadata: {
properties: {
text: { type: "string", defaultValue: "" }
},
aggregations: {
"_text": { type: "sap.m.Text", multiple: false, visibility: "hidden" }
}
},
init() {
Control.prototype.init.apply(this, arguments);
this.setAggregation("_text", new Text(this.getId() + "-text"));
},
setText(sText) {
this.setProperty("text", sText, true);
this.getAggregation("_text")?.setText(sText);
return this;
},
renderer: {
apiVersion: 4,
render(oRm, oControl) {
oRm.openStart("div", oControl);
oRm.openEnd();
oRm.renderControl(oControl.getAggregation("_text"));
oRm.close("div");
}
}
});
return SimpleText;
});
This example uses a setter wrapper for the single property, in line with the pattern shown earlier on this page. If the control you wish to migrate has many fragment bindings on {$this>...}, see Synchronizing Properties via a $this Model for a model-based replacement that preserves the binding syntax.
Related Information