Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
pascal_wasem1
Product and Topic Expert
Product and Topic Expert
16,125

Prerequisites


Ideally you should have worked with SAPUI5 and especially custom controls and fragments before.

If not, don't worry! The SAPUI5 SDK provides many helpful tutorials and walkthroughs.

Motivation


Custom controls in SAPUI5 are a great way to encapsulate and reuse functionality throughout your application or even projects.

Though there is one thing that was bothering me: the rendering of custom controls, which unfortunately has to be done programmatically. So you always have to make use of a render manager for writing HTML tags, data, styles and controls:
sap.ui.define([
"sap/ui/core/Control",
"sap/m/Text",
"sap/m/Input"
], function (Control, Text, Input) {
"use strict";
return Control.extend("sap.ui.MyControl", {
renderer : function (oRenderManager, oControl) {
oRenderManager.write("<div");
oRenderManager.writeControlData(oControl);
oRenderManager.addClass("myClass");
oRenderManager.writeClasses();
oRenderManager.write(">");
oRenderManager.renderControl(new Text({...}));
oRenderManager.renderControl(new Input({...}));
oRenderManager.write("</div>");
}
});
});

This kind of violates the MVC pattern which is a crucial part of SAPUI5 and just does not fit in the whole picture especially regarding the clean separation of XML views and controllers.

So I was wondering if it would be possible to create custom controls which use XML for rendering?

SPOILER: yes!

Using fragments for rendering custom controls


Fragments in SAPUI5 allow to reuse XML (without any controller or other behavior code involved) throughout different views. A fragment consists of one or multiple controls. So a fragment is not dissimilar to a custom control, but it resembles a view in terms of functionality.
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Text ... />
<Input ... />
</core:FragmentDefinition>

So why not combine both custom controls and fragments?

Step 1: Create the FragmentControl base class


Our main intention is to create a new FragmentControl base class which uses fragments for rendering. Other controls could then inherit from this base class and provide their own fragment for rendering and implement their own logic.

As for any custom control we start by inheriting from sap.ui.core.Control:
sap.ui.define([
"sap/ui/core/Control",
"sap/ui/core/Fragment"
], function(Control, Fragment) {
"use strict";

/**
* Base class for controls which use XML fragments for rendering
*
* @param {string} [width] - optional width of the control, defaults `100%`
* @param {object} [height] - optional height of the control, defaults to `auto`
*
* @public
* @extends sap.ui.core.Control
*/
var FragmentControl = Control.extend("sap.ui.FragmentControl", {

metadata: {

properties: {

/**
* Width
*/
width: {
type: "sap.ui.core.CSSSize",
defaultValue: "100%"
},

/**
* Height
*/
height: {
type: "sap.ui.core.CSSSize",
defaultValue: "auto"
}

}
},

/**
* Get a fragment control by its id
*
* @public
* @param {string} sControlId - the controls id
*
* @return {sap.ui.core.Control} - the found control (or a falsey value if not found)
*/
byId: function(sControlId) {
return Fragment.byId(this.getId(), sControlId);
}

});

return FragmentControl;
});

Step 2: Load the fragment


Our next step will be to load the fragment definition for each control. Luckily fragments can be instantiated programmatically. By doing so we will get an array of controls defined by this fragment.

We need to load our fragment controls in the init method of our base class and store them in a private property for accessing them later. Each control also needs to be registered as a dependent in order for enabling proper data binding.

For the actual loading of our fragment definition, we provide two distinguish ways and introduce an abstract method for each way in our base class. So each inheriting control needs to implement one of these:

  1. getFragmentName: simply return a fragment name which can be loaded by the SAPUI5 module system (e.g. sap.ui.fragment.MyControl, which points to file fragment/MyControl.fragment.xml

  2. getFragmentContent: return the XML fragment as a string (which is kind of  inspired by React). This eliminates the need to create a fragment definition and the control stands for its own.


sap.ui.define([
"sap/ui/core/Control",
"sap/ui/core/Fragment"
], function(Control, Fragment) {
"use strict";

/**
* Base class for controls which use XML fragments for rendering
*
* @param {string} [width] - optional width of the control, defaults `100%`
* @param {object} [height] - optional height of the control, defaults to `auto`
*
* @public
* @extends sap.ui.core.Control
*/
var FragmentControl = Control.extend("sap.ui.FragmentControl", {

metadata: {

properties: {

...

/**
* Fragment controls
* @private
*/
_aFragmentControls: {
type: "sap.ui.core.Control[]",
defaultValue: null
}

}
},

/**
* Initiate the control
*
* @public
*/
init: function() {
// load fragment controls
this._aFragmentControls = this._loadFragmentControls();

// connect models / enable data binding for fragment controls
this._aFragmentControls.forEach(function(oFragmentControl) {
this.addDependent(oFragmentControl);
}, this);
},

/**
* Load fragment controls
* @private
* @returns {sap.ui.core.Control[]} fragment controls
*/
_loadFragmentControls: function() {
var vFragment = null;

var sFragmentContent = this.getFragmentContent();
if (sFragmentContent) {

// load fragment content
var oFragmentConfig = {
sId: this.getId(),
fragmentContent: sFragmentContent
};
vFragment = sap.ui.xmlfragment(oFragmentConfig, this);

} else {

// load fragment by name
vFragment = sap.ui.xmlfragment(this.getId(), this.getFragmentName(), this);
}

// ensure array
var aFragmentControls = Array.isArray(vFragment) ? vFragment : [vFragment];

return aFragmentControls;
},

/**
* Get fragment name for this control.
* The fragment name must correspond to an XML Fragment which can be loaded via the module system (fragmentName + ".fragment.xml") and which defines the Fragment.
* To provide the fragment content directly please override `getFragmentContent`.
*
* @public
* @abstract
*
* @returns {string} the fragment name, e.g. some.namespace.MyControl
*/
getFragmentName: function() {
// default: fragment for control, e.g. some/namespace/MyControl.js -> some/namespace/MyControl.fragment.xml
return this.getMetadata().getName();
},

/**
* Get the fragment content for this control as an XML string.
* Implementing this method eliminates the need to create a `MyControl.fragment.xml` file,
* so e.g. `return <core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core"><Input/></core:FragmentDefinition>`
*
* If this method returns any non falsey value `getFragmentName` will be ignored.
*
* @public
* @abstract
*
* @returns {string} XML fragment
*/
getFragmentContent: function() {
// default: undefined
return;
},

...

});

return FragmentControl;
});

Step 3: Render the custom control with the FragmentControlRenderer


Each custom control needs to implement its render function or provide a renderer class. We we do the latter and create a dedicated FragmentControlRenderer. After having stored our fragment controls in a property this is just busywork:
sap.ui.define([
"sap/ui/core/Renderer"
], function(Renderer) {

var FragmentControlRenderer = Renderer.extend("sap.ui.FragmentControlRenderer", {

render: function(oRenderManager, oControl) {

// return immediately if control is invisible, do not render any HTML
if (!oControl.getVisible()) {
return;
}

// start opening tag
oRenderManager.write("<div");

// write control data
oRenderManager.writeControlData(oControl);

// write classes
oRenderManager.writeClasses();

// write styles
oRenderManager.addStyle("width", oControl.getWidth());
oRenderManager.addStyle("height", oControl.getHeight());
oRenderManager.writeStyles();

// end opening tag
oRenderManager.write(">");

// render fragment controls (@see sap.ui.fragment.FragmentControl.metadata.properties._aFragmentControls)
if (Array.isArray(oControl._aFragmentControls)) {
oControl._aFragmentControls.forEach(function(oFragmentControl) {
oRenderManager.renderControl(oFragmentControl);
});
}

// write closing tag
oRenderManager.write("</div>");
}

});

return FragmentControlRenderer;
});

Finally we need to set the renderer property in our FragmentControl base class:
sap.ui.define([
"sap/ui/core/Control",
"sap/ui/core/Fragment",
"sap/ui/FragmentControlRenderer"
], function(Control, Fragment, FragmentControlRenderer) {
"use strict";

/**
* Base class for controls which use XML fragments for rendering
*
* @param {string} [width] - optional width of the control, defaults `100%`
* @param {object} [height] - optional height of the control, defaults to `auto`
*
* @public
* @extends sap.ui.core.Control
*/
var FragmentControl = Control.extend("sap.ui.FragmentControl", {

metadata: {

properties: {

/**
* Renderer
*/
renderer: {
type: "sap.ui.FragmentControlRenderer",
defaultValue: FragmentControlRenderer
}

...

}
},

...

});

return FragmentControl;
});

Step 4: Create our own fragment control


Now we are ready to create our fragment control by simply inheriting for the FragmentControl base class and providing our own fragment.

Let's create a simple fragment:
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Text id="myText"/>
<Input change="onChange"/>
</core:FragmentDefinition>

Now we just need to provide this fragment in our own control and implement some logic:
sap.ui.define([
"sap/ui/FragmentControl"
], function(FragmentControl) {
"use strict";

/**
* My class
*
* @public
* @extends sap.ui.FragmentControl
*/
var MyControl = FragmentControl.extend("sap.ui.MyControl", {

/**
* @override
*/
getFragmentName: function() {
return "sap.ui.fragments.MyControl";
},

/**
* Handle the change event
* @param {sap.ui.base.Event} oEvent - the change event
*/
onChange: function(oEvent) {
var sValue = oEvent.getParameter("value");
var oText = this.byId("myText");
oText.setText(sValue);
}

});

return MyControl;
});

 

Conclusion


Custom controls and fragments work well together. FragmentControls are an elegant way to combine both and take full advantage of SAPUI5s separation of views and logic.

Enjoy!
4 Comments