Technical Articles
How to Extend a Standard SapUI5 Control
There comes a time in every project’s lifecycle when you realize you should start using extended controls in your XML views.
For example,using the same Value Help dialogs several times in the same project can be quite common, but copying and pasting the same code would be difficult to maintain.
Another case would be when you’re assigning a custom “displayFormat” to your datepickers repeatedly throughout your project for every datepicker in several XML views.
In this post we will take a standard control, set some of its properties, add a bit of functionality to it and then reuse it as many times as needed in our project.
Extending a SAPUI5 Standard control ,or as some people would call Inheriting from it will in the long run, simplify your code and make it easier to maintain.
In our example we will take the standard Input control and extend its functionality by :
- Attaching a value help dialog with a list of materials.
- Passing parameters from its parent XML view.
- Handling selection events.
- Adding basic input validation.
When we’re done we’ll be able to use the extended control over and over in our XML views without copying and pasting the same code in our views and controllers.
All of the above functionality will be encapsulated in a single file called “SelectMaterial.js” which will provide the new control that we can then use in our XML view like so:
<my:SelectMaterial value="{viewModel>/matnr}" lgort="3001" onSelect="onMatnrSelect" width="160px"/>
Full Source code for this project can be found here:
https://github.com/Sagi44/ExtendedSAPUI5Control
So the first step is to add a new folder called “Controls” under webapp.
Under “Controls” add a file called “SelectMaterial.js” with the following code:
sap.ui.define([
"sap/ui/core/Control",
"sap/m/Input",
"sap/m/MessageToast",
"sap/ui/model/Filter"
], function(Control, Input, MessageToast, Filter) {
"use strict";
return Input.extend("sapui5.demo.customcontrols.controls.SelectMaterial", {
metadata: {
properties: {
value: {
type: "string",
defaultValue: ""
},
text: {
type: "string",
defaultValue: ""
},
showValueHelp: {
type: "boolean",
defaultValue: true
},
lgort: {
type: "string",
defaultValue: "0000"
}
},
aggregations: {
},
events: {
"onSelect": {
allowPreventDefault: true,
parameters: {
"material": {
type: "object"
},
"materialDescription": {
type: "string"
}
}
}
},
renderer: null
},
init: function() {
Input.prototype.init.call(this);
this.attachChange(this.onChange);
this.attachValueHelpRequest(this.onValueHelpRequest);
this.attachOnSelect(this.onSelect);
},
renderer: function(oRm, oInput) {
sap.m.InputRenderer.render(oRm, oInput);
oRm.write("<Label>");
oRm.write(oInput.getText());
oRm.write("</Label>");
},
setLgort: function(sValue) {
this.setProperty("lgort", sValue, true);
return this;
},
getLgort: function() {
return this.getProperty("lgort");
},
onValueHelpRequest: function(oEvent) {
// function name speaks for itself
var that = this;
// get selected Lgort
var lgort = this.getLgort();
if (!lgort) {
MessageToast.show("Please Select Storage Location");
return;
}
var oModel = new sap.ui.model.json.JSONModel();
oModel.loadData("service/data.json");
oModel.attachRequestCompleted(function() {
that.setModel(oModel, "materials");
var oTemplate = new sap.m.StandardListItem("DialogMatnrItems", {
title: "{Material}",
description: "{MaterialDescription}"
});
var oDialog = new sap.m.SelectDialog("dialogParentTableMatnr", {
title: "Select Material",
confirm: function(oConfirmEvent) {
that.closeDialogMatnrItems(oConfirmEvent);
},
cancel: function(oCancelEvent) {
that.closeDialogMatnrItems(oCancelEvent);
},
search: function(oEvt) {
var sValue = oEvt.getParameter("value");
if (!sValue || sValue === "") {
oEvent.getSource().getBinding("items").filter([]);
} else {
var filter = new Filter("MaterialDescription", sap.ui.model.FilterOperator.Contains, sValue);
var oBinding = oEvent.getSource().getBinding("items");
oBinding.filter([filter]);
}
}
});
oDialog.setModel(that.getModel("materials"));
oDialog.bindAggregation("items", "/results", oTemplate);
oDialog.open();
});
},
closeDialogMatnrItems: function(oEvent) {
var oDialog = oEvent.getSource();
oDialog.destroyItems();
oDialog.destroy();
if (oEvent.getParameter("selectedItem") && oEvent.getParameter("selectedItem").getBindingContext()) {
var obj = oEvent.getParameter("selectedItem").getBindingContext().getObject();
// save select dialog object in control properties
this.setValue(obj.Material);
this.setText(obj.MaterialDescription);
// fire event to notify view of selection
this.fireOnSelect({
"material": obj.Material,
"materialDescription": obj.MaterialDescription
});
}
},
onChange: function(oEvent) {
// fires on focus leave or when user presses Enter
if (oEvent.type === "sapfocusleave") {
// ignore lost focus
return false;
}
if (!oEvent.target.value) {
// ignore if notempty control
return false;
}
var lgort = this.getLgort();
if (!lgort) {
MessageToast.show("Please Select Storage Location");
return;
}
// get control value and try to get its description from model
var material = oEvent.target.value;
var that = this;
var oModel = new sap.ui.model.json.JSONModel();
oModel.loadData("service/data.json");
oModel.attachRequestCompleted(function() {
that.setModel(oModel, "materials");
var oData = oModel.getData();
for (var i = 0; i < oData.results.length; i++) {
if (oData.results[i].Material === material) {
that.setValue(material);
that.setText(oData.results[i].MaterialDescription);
that.fireOnSelect({
"material": material,
"materialDescription": oData.results[i].MaterialDescription
});
return;
}
}
MessageToast.show("Material Number " + material + " Does not Exist.");
});
},
onSelect: function(oEvent) {
}
});
});
The magic begins here :
return Input.extend("sapui5.demo.customcontrols.controls.SelectMaterial"
We are inheriting all the goodness of the standard input Control and later on adding our own custom functionality.
The metadata section of our control is where we set our default properties. For example “showValueHelp” is set to true by default.
The “lgort” property is a custom property that will allow us to pass values to the control from our XML view. Its default value is “0000”.
The metadata section also defines a custom event called “onSelect” which we will fire to notify our view that the user has successfully selected a valid Material from the value help list. This event will also fire an Event object with the selected values “material” and “materialDescription”.
The next important thing to notice is the “init” function of the extended control. Here we need to attach the standard Input control’s basic events to our own custom functions.
this.attachValueHelpRequest(this.onValueHelpRequest);
In our function “onValueHelpRequest” we will query our data source of choice, populate and display a value help dialog.
Next, if the user has selected an item from the list, we will need to fire an event to let the view know about this.
this.fireOnSelect({
"material": obj.Material,
"materialDescription": obj.MaterialDescription
});
We can then handle the event in our XML view :
<my:SelectMaterial value="{viewModel>/matnr}" lgort="3001" onSelect="onMatnrSelect" width="160px"/>
and of course in our view controller :
onMatnrSelect: function(oEvent) {
// Called whenever Matnr has been changed (either by pressing Enter, lost Focus, or Value Help Selection )
var matnr = oEvent.getParameter("material");
var matnrDescription = oEvent.getParameter("materialDescription");
// do something with selection
MessageToast.show("You selected :" + matnr + " " + matnrDescription);
// view model is updated
var model = this.getView().getModel("viewModel");
MessageToast.show("Model Matnr = :" + model.getProperty("/matnr"));
}
I hope this post has given you an outline on how to build upon existing standard controls and create your own extended controls.
Let me know if you have and suggestions or remarks.
Regards,
Sagi
Full Source code for this project can be found here:
https://github.com/Sagi44/ExtendedSAPUI5Control
Hi Sagi,
thanks for the detailed tutorial. I have to admit, that although I have already developed a few custom SAPUI5 apps, I have successfully avoided custom controls until now. Mainly because of one reason: if something doesn't work, you are on your own. There is no SAP note that can repair your custom control 😉
Two questions though:
Thanks for your effort.
BR, Klaus
Hey Klaus,
the following blog might help you out or even answer what you were looking for in your 2nd question, although already being ~2yrs old.
https://blogs.sap.com/2018/01/15/step-by-step-procedure-to-create-sapui5-library-with-custom-controls-and-consume-the-library-into-sapui5-applications/
Best Regards,
Marco 🙂
Hey again,
I think the answer to question one would be that there is no need to declare the "value" property again as this is being inherited from it's parent (like sap.m.Input and sap.m.InputBase). However, I couldn't find the text property within those controls, so it makes sense declaring this within the metadata of the exstension. I'm not sure though, maybe I've overlooked something and there is a "text" property already declared somewhere.
Please correct me if I'm wrong. 🙂
Best Regards,
Marco
Great Blog Sagi Yehuday!
Much like Klaus Kronawetter, I have also successfully avoided extending standard controls so far.
However I'd like to mention that the declaration of ...
within sap.ui.define is not needed in your example (correct me if I'm wrong).
I would also like to ask if it's necessary to have the 'renderer: null' declared within the control metadata as you're defining a renderer method later on.
Thanks again for this great Blog!
Best Regards,
Marco
Marco Beier Yes you're right, the “sap/ui/core/Control” injection can be omitted as is the
“renderer: null” declaration.
Many Thanks for your feedack!
Thank you very much for your blog article. It helped me to understand the API better and gave me an idea how to extend one Component.
Your sample questions my understanding of XML.composites. Do I understand correctly, that you add a Label to your Input component within your render function? If yes, can you shortly explain why extending directly from a Component/Control is chosen before a XML.composite approach (https://sapui5.hana.ondemand.com/#/topic/b83a4dcb7d0e46969027345b8d32fd44) with (property) aggregation forwarding (https://sapui5.hana.ondemand.com/#/topic/64a5e1775bf04d4883db18c9de7d83bd). I thought XML.composite and its controller makes sense if you have to work with 1+ components within your view. And it should be chosen if you have to create a new component from several components. Maybe I misunderstood the concept.
Thanks again for your blog post!
Hey Florian Weil,
although I'm not the creator of this blog I can assure you that you've understood the concept behind XMLComposite-Controls correctly - though, don't confuse controls with components!
I think the reasoning behind manually adding the label within the renderer is to show that you're able to manually change what is rendered when creating a custom control but also refers to the fourth point of what Sagi Yehuday! wanted to demonstrate, which is 'Adding basic input validation' as he shows what our input is by displaying it within the added/rendered label control.
Hope I could help. 🙂
Best Regards,
Marco