Custom Control 101 #sapui5 #openui5 #tipoftheday #customcontrol
With the OpenUI5 community we started a new project called UI5Lab.
UI5Lab is a place to gather forces and work on a UI5 community-driven custom repository accompanied by vibrant exchange. Whatever will be placed here is discussed in Slack Channel #UI5Lab. You can join with this invitation link for Slack.
Here’s a list of all technologies we are going to use to create our custom control:
We will use NPM to downlaod and sync all the dependencies needed to dev, build and test our work.
GruntJS is a task runner build on top of NodeJS that will allow you to automate everything. The less work you have to do when performing repetitive tasks like minification, compilation, unit testing, linting, etc, the easier your job becomes.
It’s a grunt plugin developed by Matthias Oßwald that will allow you to configure task to build, test and minify your openui5 app/library.
How to start?
- Create your bower.json to download libraries needed to locally test your code via grunt connect
- Create your package.json with all the npm dependences
- Wrap the library inside your custom control, add all the attributes and events needed (and please, also add all needed documentation!)
- Create your Gruntfile.js to clean, lint and build the library
The Library: QRCodeJS
These are the reasons I’ve chose it:
- It has an NPM library, but in the worst case you can clone the repo and upload your own ;)
- No jQuery / other dependencies: that’s important because you don’t want to struggle with incompatibilities.
- Browset compatibility: E6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC.
- Lot of options to personalize your QRCode: text, width, height, colorDark, colorLight, correctLevel
How to extend OpenUI5 Controls
The metadata section defines the data structure and thus the API of the control. With this meta information on the properties, events, and aggregations of the control OpenUI5 automatically creates setter and getter methods and other convenience functions that can be called within the app.
A property is defined by a name and a type. Additionally, a default value can be defined for a property.
- type: Data type of the control property; SAPUI5 provides an automatic type validation. Valid types are, for example, string (default) for a string property, int or float for number properties, int, etc. for arrays and sap.ui.core.CSSSize for a custom-defined type.
- defaultValue: Default value that is set if the application does not set a value; if no default value is defined, the property value is undefined.
Aggregations and Associations
An aggregation is a strong relation that also manages the lifecycle of the related control, for example, when the parent is destroyed, the related control is also destroyed. Also, a control can only be assigned to one single aggregation, if it is assigned to a second aggregation, it is removed from the previous aggregation automatically.
An association is a weak relation that does not manage the lifecycle and can be defined multiple times. To have a clear distinction, an association only stores the ID, whereas an aggregation stores the direct reference to the control. We do not specify associations in this example, as we want to have our internal controls managed by the parent.
Aggregations and associations are defined by their name and a configuration object with the following information:
- type: The type should be a subclass of the element or the control; the default is sap.ui.core.control
- multiple: Defines whether it is a 0..1 aggregation or a 0..n aggregation; the default for aggregations is true = 0..n, and for associations the default is false
- singularName: For 0..n aggregations, the aggregation name typically is plural, but certain methods are created where the singular form is required (for example, addWorksetItem} for the “worksetItems” aggregation).
Multiple methods are created automatically at runtime, depending on the multiplicity, for example getWorksetItems, insertWorksetItem, addWorksetItem, removeWorksetItem, removeAllWorksetItems, indexOfWorksetItem, destroyWorksetItems. These methods have a default implementation which does everything to handle the aggregation properly, but they can be overridden and extended by the control implementation.
If you want to mark one aggregation as default aggregation in order to be able to omit the aggregation tag in XML views, you can do this by setting the defaultAggregation property to the name of the aggregation
Events allow you to expose which events will be fired by your custom control and which parameters will be shipped with it.
For each event, methods for registering, de-registering and firing the event are created.
You can gather more documentation about the metadata section here.
The renderer defines the HTML structure that will be added to the DOM tree of your app whenever the control is instantiated in a view. It is usually called initially by the core of OpenUI5 and whenever a property of the control is changed. The parameter oRM of the render function is the OpenUI5 render manager that can be used to write strings and control properties to the HTML page.
You have two different options to define your renderer:
- On-the-fly directly in your custom control (function has 2 inputs, oRM and oControl)
- Create a separate file called YourCustomControlNameRenderer.js that extends sap.ui.core.Renderer
This is a more clean and modular way in my opinion because you have all your code in a different file and allows you to better extend your custom control.
Introduction to create the control library
Now we have a basic knowledge on how a custom control work. To create and build the library we need to create these files:
- Translations: messagebundle.properties (not needed in this example)
- Themes: library.source.less, shared.css, img, img-RTL (not needed in this example)
- Control and Renderer
For a deeper explanation on the development process you can take a look a the official control library documentation.
.libraryfile is an XML file describing the library and its dependencies, as well as other information required at build time, e.g. related to tests, test coverage and documentation resources. This file is not used by the UI5 runtime
library.jsfile is a central file for each control library that contains the library declaration and any enums, simple types and interfaces present in the library. There can also be additional code and the definition of lazy loading stubs for non-controls.
The most important feature in this file from the perspective of a control developer is the list of controls maintained in the library declaration: all controls must be added there in order to make their constructors available immediately when the library is loaded (so applications only need to require the library, not each control).
Translation file (messagebundle.properties) and translation
messagebundle.propertiesfile contains all translatable texts used by controls as key-value pairs, with annotations making translator's life easier.
The Renderer: QRCodeRenderer.js
This is the renderer of our custom control. As we explained before, the renderer’s job is to render the HTML of your custom control.
To do that you need to extend the Renderer and override the render function that has two parameters:
- oRM: the RenderManager that can be used for writing to the render output buffer
- oControl: an object representation of the control that should be rendered
The RenderManager offers a lot of APIs to manipulate and write the HTML. In this example we’re just using the basic one:
- write(sText): write the given texts to the buffer
- writeEscaped(sText, bLineBreaks): Escape text for HTML and write it to the buffer.
- writeControlData(oControl): writes the controls data into the HTML. Control Data consists at least of the id of a control. This should be used when you get the data to be written from somewhere else, in order to avoid cross-site-scripting issues.
- addClass(sName): adds a class to the class collection if the name is not empty or null. The class collection is flushed if it is written to the buffer using writeClasses
- writeClasses(): writes and flushes the class collection (all CSS classes added by “addClass()” since the last flush). Also writes the custom style classes added by the application with “addStyleClass(…)”.
- renderControl(oControl): Turns the given control into its HTML representation and appends it to the rendering buffer.
The Custom Control: QRCode.js
This is the main file of our custom control. When you want to wrap an external library you have to do these few things:
- Add needed properties inside the metadata section
- Add needed aggregations inside the metadata section
- Add needed events inside the metadata section
- Ovverride the lifecycle callbacks if needed: init. onBeforeRendering, onAfterRendering, exit
- Override getter/setter of your properties if needed. Remember to always return the object instance to allow method chaining
- Initialize your the qrcode library
In our custom control, we’ve mapped all the qrcode library with our metadata.properties. This allow us to take advantage of the two-way-binding mechanism offered by sapui5. Here’s an example:
We have only an aggregation, named __qrcodeHTML that allows us to store the QRCode canvas object. We’ve choosen to use a sap.ui.core.HTML in order to avoid to create/destroy each time our static canvas.
In our case we’ve no events defined inside our metadata because there was no need. If you want you can take a look at the FlatDatePicker example to see how to define an event and how to fire it.
In our init function we assign our aggregation with a static HTML. In the onAfterRendering we check if the __qrcode variable is already instantiated, otherwise we create it from scratch.
Now we’re ready to integrate our custom control (after we’ve builded our custom control library).
Configure BowerJS: bower.json
This is the configuration file used by BowerJS do download all the needed dependencies for your project. This task is executed automatically by npm install but you can manually do it via bower install command.
Configure NPM: Package.json
This is the configuration file used by NPM to know which dependences you need to download when you execute npm install
There are 2 main section you need to pay attention to:
- scripts: those are 2 automated commands that will be exectued after npm install (it will also download all bower dependences need by grunt connect, something that I will explain later).
- devDependencies: these are all the libraries that npm will install, with a specific version. All these libraries will be downloaded locally to a folder called node_modules
Configure GruntJS: Gruntfile.js
- openui5_connect will let you configure grunt-connect to run a local webserver and test your library without apache/tomcat (yay!)
- openui5_preload will let you merge multiple files to build the final library/component
You can find all the information you need on the project documentation. Maybe I will do a separate post to create a Component-preload.js ;)