Standard controls cover most screens, but sometimes you need something the library doesn't offer — say you have a rating widget with a custom animation, or a tag cloud with domain-specific behavior. That's when you build a custom control. It's less intimidating than it sounds: extend a base control, define a renderer, add properties. Let's build one.
The anatomy of a custom control
A custom control has two parts: the control class (API, properties, events) and the renderer (HTML output). Keep them separate — logic in the control, markup in the renderer.
sap.ui.define([
"sap/ui/core/Control"
], (Control) => {
"use strict";
return Control.extend("my.app.control.StarRating", {
metadata: {
properties: {
value: { type: "int", defaultValue: 0 },
max: { type: "int", defaultValue: 5 }
},
events: {
change: { parameters: { value: { type: "int" } } }
}
},
setValue: function (iValue) {
this.setProperty("value", iValue, true); // suppress re-render
this.fireChange({ value: iValue });
return this;
},
renderer: {
apiVersion: 2,
render: function (oRm, oControl) {
oRm.openStart("div", oControl)
.class("starRating")
.openEnd();
for (let i = 1; i <= oControl.getMax(); i++) {
oRm.openStart("span")
.class(i <= oControl.getValue() ? "star on" : "star")
.attr("data-value", i)
.openEnd()
.text("★")
.close("span");
}
oRm.close("div");
}
}
});
});
Notice apiVersion: 2 — the modern renderer API. The old string-concatenation style is deprecated; use the semantic rendering methods.
Wiring up interaction
Rendering is half the job. Attach DOM events in onAfterRendering so the stars are clickable.
onAfterRendering: function () {
this.$().find(".star").on("click", (oEvent) => {
this.setValue(parseInt(oEvent.currentTarget.dataset.value, 10));
});
},
Because setValue fires the change event, parent views can bind to it like any standard control event. Your custom control now behaves as a first-class citizen.
Always fire events for state changes instead of expecting parents to poll. Consumers of your control should never need to know its internals.
Using it in a view
Reference your control's namespace in the XML view and use it like a built-in control, including data binding.
<mvc:View xmlns:custom="my.app.control" ...>
<custom:StarRating
value="{product>Rating}"
max="5"
change=".onRatingChange"/>
</mvc:View>
Binding works automatically because the property follows the standard getter/setter conventions from the metadata.
Before you build: check twice
- Composite instead: if the widget is just a combination of existing controls (a Label plus an Input), build a composite — an XML fragment or a control that aggregates others — rather than a renderer from scratch.
- Extend, don't reinvent: need a special table? Extend
sap.m.Tableinstead of rebuilding one. - Style with CSS: keep all visuals in a stylesheet. The renderer should emit semantic markup and classes, not inline styles.
Build custom controls for genuinely new behavior, not for styling. Nine times out of ten, CSS plus a composite gets you there with far less code.
What custom control are you planning to build? Describe it in the comments.