Skip to Content

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!

To report this post you need to login first.

3 Comments

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

  1. Andreas Kunz

    Nice blog implementing a good idea. An idea so good that it is also being implemented in UI5 itself (late, but better than never). Coincidentally under the name “FragmentControl”. 😉

    There has been a presentation at UI5con@SAP 2017 about it, you can find the slides here, the code examples here, and the running demos here.

    The implementation is flagged as “experimental” (and hidden in the documentation) in UI5 1.48 and should be officially released soon after, but can be tried already.

    While your approach allows wrapping an XML fragment in a control, it does not allow having properties, aggregations etc. on the API of the new control, which are forwarded to some of the inner controls. This is probably needed in real-world examples. This problem is solved in a very convenient way in our “original” FragmentControls, using the (also new) ManagedObjectModel, which allows simply binding properties and aggregations in the inner XML to the outer API (see the examples).

     

    Please note that using the “sap.ui” namespace for your own development is not recommended, as it may conflict with new things provided by UI5.

     

    Also a comment regarding:

    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 […] This kind of violates the MVC pattern 

    This is a true in those cases where a control is just a composition of other controls. But many controls (or custom controls, which are technically the same thing) do have to create some HTML on their own. After all, that’s what browsers render. When you look at what controls in a Fragment or XMLView do, or what their inner controls do, sooner or later all they do is creating HTML.

    Of course there are also declarative ways to describe the HTML for a control, like templates. That’s something we have looked at very early on (like 8 years ago) and discarded at that time because for real-life controls the templates would have become very complex and unreadable due to many dynamic parts. Nevertheless this is being re-considered now in the context of “UI5 evolution“.

     

    Thanks again for the blog… good inventions often happen simultaneously at different places. 😉

     

    (1) 
    1. Pascal Wasem Post author

      Hi Andreas,

      thanks for your comment. Unfortunately I could not attend UI5con. I knew I would be missing something!

      FragmentControls being implemented in UI5 itself its just great news!!!

      Your have a good point regarding properties and especially aggregations in my showcase, but having them also being taken care of  is even better!

      I am really looking forward to create my own FragmentControl using the official UI5 approach 🙂

       

      Regards,

      Pascal

       

      (0) 
    2. Andreas Kunz

      An update because many might find this post when looking for fragment controls in UI5:

      We have decided today to rename the UI5 implementation to “XMLComposite”. While “FragmentControl” was a technically accurate name, several users were confused because they thought these controls were in some way incomplete (fragments). The renaming is already on the way.

      As internally the 1.50 release development is already closing (even though 1.48 is not yet released publicly), and because some parts were still incomplete or not robust enough, it was also decided not to officially release this new feature in 1.50. A possible (and likely, but not promised – everything may still be changed or removed, as always…) target is 1.52.

      (0) 

Leave a Reply