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>





switch1.PNG

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

switch2.PNG

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!

switch3.PNG

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"
                    />




switch4.PNG

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

switch5.PNG

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!"
                    />




switch6.PNG

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

switch7.PNG

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
To report this post you need to login first.

4 Comments

You must be Logged on to comment or reply to a post.

  1. Nigel James

    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.

    (0) 
  2. Parepalli Shravan Goud

    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.

    (0) 
  3. Mario Papadopoulos

    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?

    (0) 

Leave a Reply