How to extend a standard UI5 control
Getting Started – Introduction
I recently walked through creating custom controls
Now I’m going to also walk through extending the standard library controls (without destroying things 😏 )
Today I’m going to extend the Switch control (because I need to anyway, everybody wins!)
In this case, I am adding the editable property that many other sap.m controls already have (like sap.m.Input)
This is important for accessibility compliance (because disabling input controls causes them to grey out, which fails contrast colour tests, and can also cause screen readers to miss the control entirely). Also it looks much nicer when you switch a form between edit and read modes.
So instead of disabling the Switch I’ll be able to set editable to false, which will make its State render as plain text (just like sap.m.Input)
I’m going to start with a standard sap.m.Switch in my xml view as a point of reference
<mvc:View xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:dalraeContainers="dalrae.ui.containers"
xmlns:core="sap.ui.core"
xmlns:f="sap.ui.layout.form"
controllerName="dalrae.doco.Page">
<Page title="D'Alrae UI5 Library" enableScrolling="true">
<headerContent>
</headerContent>
<content>
<dalraeContainers:ShadowBox width="150px">
<f:SimpleForm>
<Label text="Are you ok?" />
<Switch state="false"
customTextOn="Yes"
customTextOff="No"
/>
</f:SimpleForm>
</dalraeContainers:ShadowBox>
</content>
</Page>
</mvc:View>
Extending the Control
So now I want to extend this control and (hopefully) have the view look exactly the same as it did with the original sap.m.Switch
When we create new controls we extend the blank sap.ui.core.Control class, but in this case we extend sap.m.Switch
But the concept is the same, the only difference being that we need to use the existing renderer of sap.m.Switch
Here’s what that looks like
sap.ui.define(
['sap/m/Switch'],
function(Switch) {
return Switch.extend("dalrae.ui.controls.Switch",{
metadata: {
properties: {
editable: {
type: "boolean",
defaultValue: true
}
}
},
renderer: function(oRm,oControl){
sap.m.SwitchRenderer.render(oRm,oControl); //use supercass renderer routine
},
});
}
);
The unintuitive thing here is that, the renderer ends up in its own class SwitchRenderer, instead of Switch.prototype.renderer, tough to figure out without seeing an example.
Now that I have my own Switch control, I can just change the namespace in my xml, and because I’m calling the correct renderer, the control will still work correctly. (I’ve also set the state to true so that I can tell I’m looking at the latest code)
<mvc:View xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:dalraeContainers="dalrae.ui.containers"
xmlns:dalrae="dalrae.ui.controls"
xmlns:core="sap.ui.core"
xmlns:f="sap.ui.layout.form"
controllerName="dalrae.doco.Page">
<Page title="D'Alrae UI5 Library" enableScrolling="true">
<headerContent>
</headerContent>
<content>
<dalraeContainers:ShadowBox width="150px">
<f:SimpleForm>
<Label text="Are you ok?" />
<dalrae:Switch state="true"
customTextOn="Yes"
customTextOff="No"
/>
</f:SimpleForm>
</dalraeContainers:ShadowBox>
</content>
</Page>
</mvc:View>
And upon retest… hooray! my Switch is still working
Extending the Control – Two Renderers
Now the real work, to toggle between the default renderer and my own renderer.
When editable is false, the render will be custom
sap.ui.define(
['sap/m/Switch'],
function(Switch) {
return Switch.extend("dalrae.ui.controls.Switch",{
metadata: {
properties: {
editable: {
type: "boolean",
defaultValue: true
}
}
},
renderer: function(oRm,oControl){
if(oControl.getEditable())
sap.m.SwitchRenderer.render(oRm,oControl); //use supercass renderer routine
else {
//render control as simple text
var txt = (oControl.getState() ?
oControl.getCustomTextOn() : oControl.getCustomTextOff()); //determine the text based on on/off state
oRm.write("<span tabindex=\"0\""); //tabindex allows keyboard navigation for screen readers
oRm.writeControlData(oControl); //ui5 trackings data, outputs sId, absolutely mandatory
oRm.writeClasses(oControl); //allows the class="" attribute to work correctly
oRm.write(">");
oRm.write( jQuery.sap.encodeHTML( txt ) ); //always use encodeHTML when dealing with dynamic strings
oRm.write("</span>");
}
},
});
}
);
And a slight change to the xml to set editable to false
<dalrae:Switch state="true"
customTextOn="Yes"
customTextOff="No"
editable="false"
/>
And look at that, nailed it!
My Switch is now not editable, and displays in a universally accessible way!
Extending the Control – Check for bugs, read the source
Unfortunately though, you may have already figured out a scenario where this code bugs out…
If I take out the customTextOn and customTextOff properties… my render is blank
<dalrae:Switch state="true"
editable="false"
/>
So I need to handle this scenario and get the default On/Off text rendering
Now I *could* just hardcode this… but let’s not mess with something that SAP have probably done smarter already
I’m sure I can find that text somewhere, so I’m going to just google for the sap.m.Switch source code and see what’s going on, and here it is: https://searchcode.com/codesearch/view/92988873/
(you can also use debug mode in your fiori app to find the -dbg file, i find googling faster in most cases)
And it didn’t take me long to find this little snippet in there…
sap.m.Switch.prototype.onBeforeRendering = function() {
var Swt = sap.m.Switch;
this._sOn = this.getCustomTextOn() || Swt._oRb.getText("SWITCH_ON");
this._sOff = this.getCustomTextOff() || Swt._oRb.getText("SWITCH_OFF");
So there we go, as suspected, there’s some intelligence going on there (possibly for language definitions)
Reusing that should be as simple as using those variables they’ve set (this._sOn and this._sOff) which occurs before the render
New code:
sap.ui.define(
['sap/m/Switch'],
function(Switch) {
return Switch.extend("dalrae.ui.controls.Switch",{
metadata: {
properties: {
editable: {
type: "boolean",
defaultValue: true
}
}
},
renderer: function(oRm,oControl){
if(oControl.getEditable())
sap.m.SwitchRenderer.render(oRm,oControl); //use supercass renderer routine
else {
//render control as simple text
var txt = (oControl.getState() ?
oControl._sOn : oControl._sOff); //use the super classes existing variables for on/off text
oRm.write("<span tabindex=\"0\""); //tabindex allows keyboard navigation for screen readers
oRm.writeControlData(oControl); //ui5 trackings data, outputs sId, absolutely mandatory
oRm.writeClasses(oControl); //allows the class="" attribute to work correctly
oRm.write(">");
oRm.write( jQuery.sap.encodeHTML( txt ) ); //always use encodeHTML when dealing with dynamic strings
oRm.write("</span>");
}
},
});
}
);
And upon retest… there’s my default “On” text
And there we go, bug fixed, it’s always a good idea to check the original controls source code before banging in your own code, which could yield unpredictable results.
Before Release – Do some regression testing
I’ve made a number of changes, so it’s time to do a regression test… to ensure the custom text still works (which we know it will…)
<dalrae:Switch state="true"
editable="false"
customTextOn="Yeah!"
/>
And another test.. setting editable to true to ensure the default render still functions…(I know it will, but retest it anyway)
<dalrae:Switch state="true"
editable="true"
customTextOn="Yeah!"
/>
Sure does
All done! Switch control extended to have an editable property
[Update 1.1, 19/09/2016]
When I actually used this control I did come across two things not working as I had hoped, I have updated the code to fix them as follows
– changing between editable false/true was not rerendering the dom
– clicking on the uneditable label was still changing the value because the underlying classes methods were not suppressed
new code below is commented with the changes
sap.ui.define(
['sap/m/Switch'],
function(Switch) {
return Switch.extend("dalrae.ui.controls.Switch",{
metadata: {
properties: {
editable: {
type: "boolean",
defaultValue: true
}
}
},
renderer: function(oRm,oControl){
if(oControl.getEditable())
sap.m.SwitchRenderer.render(oRm,oControl); //use supercass renderer routine
else {
//render control as simple text
var txt = (oControl.getState() ?
oControl._sOn : oControl._sOff); //use the super classes existing variables for on/off text
oRm.write("<span tabindex=\"0\"");
oRm.writeControlData(oControl); //ui5 trackings data, outputs sId, absolutely mandatory
oRm.writeClasses(oControl); //allows the class="" attribute to work correctly
oRm.write(">");
oRm.write( jQuery.sap.encodeHTML( txt ) ); //always use encodeHTML when dealing with dynamic strings
oRm.write("</span>");
}
},
/* UPDATE 1.1 a few bugfixes for issues I found when actually usuing this control in a project */
//force a rerender instead of writing logic to update the DOM
_setDomState: function(p) {
this.rerender();
},
//don't allow the input events to fire in editable=false mode
ontouchstart: function(e) {
if(this.getEditable())
sap.m.Switch.prototype.ontouchstart.call(this,e);
},
ontouchmove: function(e) {
if(this.getEditable())
sap.m.Switch.prototype.ontouchmove.call(this,e);
},
ontouchend: function(e) {
if(this.getEditable())
sap.m.Switch.prototype.ontouchend.call(this,e);
},
ontouchcancel: function(e) {
if(this.getEditable())
sap.m.Switch.prototype.ontouchcancel.call(this,e);
},
onsapselect: function(e) {
if(this.getEditable())
sap.m.Switch.prototype.onsapselect.call(this,e);
},
});
}
);
See you later…
This was a continuation of my blog How to create a custom UI5 control
Next up, I will expand on this topic and demonstrate
- How to add a fully accessible “press” event
Brilliant blog series Phil.
I would make one small comment on the finding the source code malarky.
I keep a local fork of openui5 on my local machine and with a great editor like sublime text it is blindingly fast to search the entire codebase. Sometimes you just need to dig in there to see how things have been implemented.
Oh and in a really hairsplitting type of way the YE... is less accessible - or at least that is what our accessibility tester keeps telling me.
Keep up the great blogging. Looking forward to the next one.
This is very well documented Phillip. I've recently extended a "Panel" element to make the entire Header clickable. It worked, but I haven't tested it rigorously. I've read this document to see if I could find something new or different and I did.
1. Your approach of handing the IF and ELSE.
2. Your Update 1.1
Thank you.
Very nice tutorial!
There is a problem though and I wonder if you or anybody else can help me:
"Error: Cannot add direct child without default aggregation defined for control"
When I follow this tutorial 1:1 and I want to extend the sap.m.PlanningCalendar, then I get this error. It also does not help to add a defaultAggregation to the extended object.
Any solutions?
Hi Mario, defaultAggregation should technically be the answer to that
My other article has an example of an aggregation (which is set as default)
Maybe compare the code from that with what you have
https://blogs.sap.com/2016/07/18/how-to-create-a-custom-ui5-control/