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:
- Only we want to modify / override some appearance (colors, fonts, size, etc). SAPUI5 lets us load custom stylesheets and add classes to our controls. See:
- Only we want to extend partially some control behavior, add new events, modifify 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: {} }, 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 and aggregations:
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 }
6. Initialize control:
This method will be called when an AutoCompleteValueHolder is instantiated
init: function() { //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+/+o2atw2AsJqpKsVysOtmWxsCYAjkmXjkcVqoWx0b28xHTUBw3tuiZJm8U1puac3LPIUaALeP2c6Xnk5VAfXicEJdam3JXGw1M3MdtqqAWulLqlxvyvlnymUw3PZEYSHVpa4l4i4gADEcj7krfT3qSXu8MBCclXFNA+AqGDeP2je6dxkHyOnT/U437AMYF+Ivm5WhPW8wrGmMBIMaeBCI/wB5M5PywZXUzgAAAABJRU5ErkJggg==', press: function(event){ var valueLayout = event.getSource().getParent(); var autoCompleteHolderLayout = event.getSource().getParent().getParent().getParent().mAggregations._layout; autoCompleteHolderLayout.removeContent(valueLayout); }, 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(); //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); } this.getAggregation("_layout").rerender(); //ReRenders _layout aggregation }
9. Final result:
JS Bin - Collaborative JavaScript Debugging
Any suggestion or feedback will be welcome
Enjoy!