How to create custom control from scratch
Why create a custom control
SAPUI5 offers several simple controls like TextField, Label… and really complex controls like ThingCollecion, Table, etc. All SAPUI5 controls are listed here:
https://sapui5.netweaver.ondemand.com/sdk/#content/Controls/index.html
It is not necessary create a custom control from scratch if:
- we only want to modify / override some appearance (colors, fonts, size, etc). SAPUI5 lets us load custom stylesheets and add classes to our controls. See:
- jQuery.sap.includeStyleSheet https://sapui5.netweaver.ondemand.com/sdk/#docs/guide/ThemingFAQ.html
- addStyleClass https://sapui5.netweaver.ondemand.com/sdk/#docs/api/symbols/sap.ui.core.Control.html#addStyleClass
- we only want to extend partially some control behavior, add new events, modify appearance not reachable by CSS, etc. See:
- Examples for Extending Existing Controls: https://sapui5.netweaver.ondemand.com/sdk/#docs/guide/OnTheFlyControlDefinition.html
sap.ui.commons.Button.extend("MyButton", { //inherit Button definition
metadata: {
events: {
"hover" : {} //new event definition hover
}
},
//hover event handler
onmouseover : function(evt) {
this.fireHover();
},
renderer: {} //Standard renderer method is not overridden
});
When our requirements doesn’t fit standard SAPUI5 controls and we have no choice we can create custom controls.
What is a control and how it works
A control defines its appearance and behavior. All SAPUI5 controls extend from sap.ui.core.Control. In the other hand sap.ui.core.Element are parts of Controls but without renderer method. For example a Menu (Control) has different MenuItem (Element) and Menu renders its MenuItems.
Main structure of controls:
- properties. Allows define its appearance and behavior on initialization.
- aggregations. Lets group controls, variables, etc. Lets define some kind of containers inside a control. For example sap.ui.table.Table has different aggregations like columns, rows, etc.
- associations. Controls can be associated with others that are not part of them. For example if we want to render a collection with next/prev functionality we could develop a previousItem / nextItem associations.
- events. Control events should be related to higher level events more than standard DOM events (click, mouseover, etc). For example if we develop a Control which renders many tabs, tabSelect could be an event when a tab is selected.
- appearance. Definition of our control in screen area. Every control has a render method in order to be rendered in HTML code.
My requirement: Custom autocomplete field like Google Gmail recipients.
I need a control that lets us:
- add/remove different values
- find values with autocomplete function
- see all added values
Example Google Gmail recipients field:
Step by step
1. Create new library AutoCompleteValueHolder in new package /control:
2. Define a basic template on AutoCompleteValueHolder.js in order to test if it works:
sap.ui.core.Control.extend("control.AutoCompleteValueHolder", {
metadata : {
properties: {},
aggregations: {},
events: {}
},
init: function() {
},
renderer : {
render : function(oRm, oControl) {
oRm.write('Hello, this is a new control :)');
}
}
});
3. Load your control library in html file:
sap.ui.localResources('control');
jQuery.sap.require("control.AutoCompleteValueHolder");
4. Use your new control in a view:
var yourNewControl = new control.AutoCompleteValueHolder('yourNewControl');
5. Add custom properties, aggregations and events:
metadata : {
properties: {
"codePropertyName": {type : "string", defaultValue: "code"}, //Define a model property representing an item code
"descriptionPropertyName": {type : "string", defaultValue: "description"}, //Define a model property representing an item description
"path": {type : "string", defaultValue: "/"}, //Define our model binding path
"model": {type : "any", defaultValue: new sap.ui.model.json.JSONModel()} //Define our model
},
aggregations: {
"_layout" : {type : "sap.ui.layout.HorizontalLayout", multiple : false, visibility: "hidden"} //Grouping of selected items and search text field
},
events : {
"selectedItem": {},
"deletedItem": {},
"deletedAllItems": {}
}
}
6. Initialize control:
This method will be called when an AutoCompleteValueHolder is instantiated
init: function() {
var oControl = this;
//Creation of search autocomplete field
var searchField = new sap.ui.commons.AutoComplete(this.getId() + '-searchField',{
maxPopupItems: 5,
displaySecondaryValues: true,
change: function change(event) {
if (event.mParameters.selectedItem != null) { //If user selects a list item, a new item is added to _layout aggregation
//Every new item consist in a TextField and a Image
var newValueField = new sap.ui.commons.TextField({
width: '100px',
editable: false
});
//TextField shares model with other TextFields and Autocomplete. We select correct path (user selection)
newValueField.setModel(event.getSource().getModel());
newValueField.bindProperty("value", event.getSource().getParent().getParent().mProperties.descriptionPropertyName);
newValueField.bindElement(event.mParameters.selectedItem.oBindingContexts.undefined.sPath);
newValueField.addStyleClass('autoCompleteValueHolder_valueField'); //Custom style
//Image let's us delete an Item. Press event will destroy TextField and Image from _layout aggregation
var newValueImage = new sap.ui.commons.Image({
src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gIKEw06nF/eLQAAAb9JREFUKM9lkkFIFHEUxn//mf3TrJVOLjvu7phBBB0M3VyNTYg85CU6ZLfqmpAwRVBseqg1CI0OzZ47ePRkh9xQiAgWgqIOXUpNhHbBDGdFSxYX2m3+HZoNpe/yHt/73oP3vgfADHsxI8T/XCOZDuIkGHO9SWcMZKM2CnL+VMqZAGO3lgkw8rFDBe/+qMrbllsFsQhiNhZxy9kxlbetwmTQpANcgesnhy6MSF+iW9H0sw2vWtwf7u8aOHsvrCRmsvNI+e2H9Vl4rwOcgI/11dJBX5I+IJtQLU3nTCs6aDVH+L72lYVXr3NLlZ1Hb8DXp4A74C9Xay+P/9zaEeLXoFnXCdcV3nqR4ufFuw/LP7LP4fcUECrJENTqfAKqvo+/+o2atw2AsJqpKsVysOtmWxsCYAjkmXjkcVqoWx0b28xHTUBw3tuiZJm8U1puac3LPIUaALeP2c6Xnk5VAfXicEm3JXGw1M3MdtqqAWulLqlxvyvlnymUw3PZEYSHVpa4l4i4gADEcj7krfT3qSXu8MBCclXFNA+AqGDeP2je6dxkHyOnT/U437AMYF+Ivm5WhPW8wrGmMBIMaeC wB5M5PywZXUzgAAAABJRU5ErkJggg==',
press: function(event){
var valueLayout = event.getSource().getParent();
var autoCompleteHolderLayout = event.getSource().getParent().getParent().getParent().mAggregations._layout;
autoCompleteHolderLayout.removeContent(valueLayout);
//Fire deletedItem event
oControl.fireDeletedItem({
allItems: oControl.getSelectedValues()
});
},
width: '12px'
});
newValueImage.addStyleClass('autoCompleteValueHolder_valueImage'); //Custom style
//Wrapping container for TextField and Image
var valueLayout = new sap.ui.layout.HorizontalLayout({content: [newValueField, newValueImage]});
valueLayout.addStyleClass('autoCompleteValueHolder_valueLayout');
//Insert wrapping layout into 0 position
event.getSource().getParent().getParent().mAggregations._layout.insertContent(valueLayout, 0);
var content = event.getSource().getParent().getParent().mAggregations._layout.getContent();
//fire selectedItem event
oControl.fireSelectedItem({
newItem: {
code: event.mParameters.selectedItem.mProperties.additionalText,
description: event.mParameters.selectedItem.mProperties.text
},
allItems: oControl.getSelectedValues()
});
//Reset value from autocomplete search field
var search = content[content.length-1];
search.setValue('');
}
}
});
searchField.addStyleClass('autoCompleteValueHolder_search'); //Custom style
//_layout aggregation creation
var layout = new sap.ui.layout.HorizontalLayout(this.getId() + '-valuesLayout',{allowWrapping: true});
layout.addContent(searchField);
layout.addStyleClass('autoCompleteValueHolder_valuesLayout');
//Set _layout aggregation into our control
this.setAggregation("_layout", layout);
}
7. Control rendering:
This method will produce html code:
renderer : {
render : function(oRm, oControl) {
var layout = oControl.getAggregation("_layout");
layout.getContent()[0].setModel(oControl.getModel());
var template = new sap.ui.core.ListItem({
text: "{"+oControl.getDescriptionPropertyName()+"}",
additionalText: "{"+oControl.getCodePropertyName()+"}"
});
layout.getContent()[0].bindItems(oControl.getPath(), template);
oRm.write("<span");
oRm.writeControlData(oControl);
oRm.writeClasses();
oRm.write(">");
oRm.renderControl(layout); //Reuse standard HorizontalLayout render method.
oRm.write("</span>");
}
}
8. Custom methods
This custom methods lets us get selected items or clear all selected items:
getSelectedValues: function() {
var content = this.getAggregation("_layout").getContent();
var result = [];
if (content != null && content.length > 1) {
//Get all selected item into result
for (var i=0; i<content.length-1; i++) {
var model = content[i].getContent()[0].getModel();
var path = content[i].getContent()[0].getBindingContext().sPath;
result.push(model.getProperty(path));
}
}
return result;
},
clearSelectedValues: function() {
if (this.getAggregation("_layout").getContent() != null && this.getAggregation("_layout").getContent().length > 1) {
//Delete all selected items (SubLayouts containing TextField+Image) from _layout aggregation
while (this.getAggregation("_layout").getContent().length > 1) {
this.getAggregation("_layout").removeContent(0);
//fire deletedAllItemsEvent
this.fireDeletedAllItems({});
}
this.getAggregation("_layout").rerender(); //ReRenders _layout aggregation
},
updateModel : function (newModel, newPath, codePropertyName, descriptionPropertyName) {
this.setModel(newModel);
this.setPath(newPath);
this.setCodePropertyName(codePropertyName);
this.setDescriptionPropertyName(descriptionPropertyName);
var layout = this.getAggregation("_layout");
layout.getContent()[0].setModel(this.getModel());
var template = new sap.ui.core.ListItem({
text: "{"+this.getDescriptionPropertyName()+"}",
additionalText: "{"+this.getCodePropertyName()+"}"
});
layout.getContent()[0].bindItems(this.getPath(), template);
}
9. Final result:
JS Bin – Collaborative JavaScript Debugging
EDIT (11/02/2014): Added some enhancements (three new events, and repetead item control).
Any suggestion or feedback will be welcome 🙂
Enjoy!
Hi Angel,
Great blog, I think it would be great to get some guidelines/standards for creating custom controls, similar to the jQuery plugins.
I guess it may come as OpenUI5 develops, but would also be great to have somewhere to share custom controls (perhaps github to start with), rather than everyone reinventing the wheel.
Many thanks,
Jason
Hi Jason,
Thanks. Sincerely It was a little bit confusing trying to understand how manager custom controls.
It was very helpful download OpenUI5 source code to understand how to manage aggregations/properties in order to renderize your custom control. This link is very useful:
OpenUI5
Kind regards!
Hi Angel,
Thanks, agree creating custom controls is quite complicated, I've experimented a little myself.
Custom controls is one area where I can see people contributing/reusing now that the library has been open sourced.
The current document outlines how to create a basic control, but would be good to define some standards, naming conventions, classnames etc for custom controls, to ensure there is some consistency.
Many thanks,
Jason
Well written and documented! THANKS!
Great Blog
Very well written and very well explained
Thanks for sharing
Regards,
Vivek
Hi Jason,
Thank you for the great blog.
How can we add CSS to the drop down suggestion that appears?
Regards,
Atul
Hi Atul,
This is Angel's blog, not mine.. 🙂
Have you tried overriding the .sapUiLbx class?
Regards,
Jason
Hi,
Great block..
I am new sapui5 and want to extend autocomplete for default suggest values on focusin.
Hi Jason,
Thanks for the Blog.
Had to make the below minor change to render to make it work when hiding and unhiding the control with values present in the control. I have placed the new control in a VBOX.
render : function(oRm, oControl) {
v[v.length-1].setModel(oControl.getModel());
var template = new sap.ui.core.ListItem({
text: "{"+oControl.getDescriptionPropertyName()+"}",
additionalText: "{"+oControl.getCodePropertyName()+"}"
});
v[v.length-1].bindItems(oControl.getPath(), template);
}
},
Thanks,
Arun Sambargi
Dear Angel,
Thanks for your blog. We have 1.18.12 (HANA SP7) and our business is in need to sap.m.MultiComboBox, but which is available from version 1.22 (HANA SP8). I am trying to copy the JS files namely sap.m.MultiComboBox and it's dependant sap.m.MultiComboBoxRenderer to local control folder as you mentioned in your blog, but unable to create a new control out of it.
When i try to create a control, I am getting below error.
"Uncaught TypeError: undefined is not a function"
Since the error is from sap-ui-core.js, I am not able to find the exact reason. Whether upgrading the sap.m library to latest version in our system would help ? If yes, any specific OSS note that would update that ? It would be of great help, if you can please help me to solve this issue.
Thanks and Regards,
Gaurav.
Thanks for the blog Angel. Have a question. Why did you keep '_layout' in aggregations? Since there will be one value anytime, don't you think it fits better in properties?
As a rule of thumb, controls should always(!) be passed as aggregations and not as properties. Using an aggregation, a parent-child relation is created, data binding changes are populated, events are fired the the control properly re-rendered if require. If you used a property instead, you would need to take care of all these things yourself 😉
- Max
You helped me a lot, Angel, thanks. I'm still stuck on some minor things, but your blog gave me a big push forward.
You are welcome! It was really hard to build the first control and then I thought it would help post it on the scn. It is not usual create custom controls, but sometimes it is required.
Kind regards
autocomplete is depricated now. what we have another option in order to achieve this?