Using roundedTiles as “menus” in UI5
Hi again 🙂
Some of you might have read my previous post about the Rounded Tiles in UI5 and wonder what more can be done with those…
If you are not familiar with OpenUI5, go and check the great work they are doing on OpenUI5
I worked a bit on the main idea that drove me creating those round tiles and I’m happy to share the next step with you…
When I started the customisation of the tiles to be able to show as round tiles, I had an idea of what I wanted to accomplish next. I wanted to have tiles that acts as “menus” having some actions attached to them… As it can’t be explained better than with a screenshot of the final result, here it is:
(Of course the different tiles colours are just for the purpose of this blog to show the display customising I implemented)
So basically, a tile can have what I will further in this blog call “actions” that are some kind of “baby tiles” on which the user press to navigate to a specific view…
It allows the grouping of actions about the same subject under the same tile and, at the same time, reduce the number of tiles necessary in the launch page of your UI5 app. The control can handle up to seven actions without any overlap.
I’ll not cover here the entire source code but only the most important parts of it. Please refer to the GitHub link at the end of the post for the entire source code, and the JSFiddle for a live demo.
What follows is very technical and if you don’t really care and just want to know how to use the control, scroll down till point 2.
1) The control
The code of the control can be found under WebContent / controls / roundedActionTile. This folder contains 2 files: the javascript code for the roundedActionTiles and the CSS attached to the control.
1.1) The JS code
As usual, at the beginning of the file some lines declares the control, attach the control specific css file (see 1.2 bellow) and requires the necessary standard libraries.
jQuery.sap.declare("controls.roundedActionTile");
jQuery.sap.includeStyleSheet("controls/roundedActionTile/roundedActionTile.css");
jQuery.sap.require("sap.m.StandardTile");
jQuery.sap.require("sap.ui.core.IconPool")
The control inherits from the sap.m.StandardTile object in order to keep the functionalities available for that object.
It extends the StandardTile by adding some properties (iconColor, cssClass) and the control has an attribute aTileOptions that is an array that will contain the actions attached to the tile.
metadata :{
properties : {
// Icon color property with default value to standard UI5 blue
iconColor : {
type : "string",
defaultValue : "#007cc0"
},
cssClass:{
type : "string",
defaultValue: "defaultActionClass"
}
},
events:{
actionPress : {}
}
},
aTileOptions: null,
The biggest part of the work is, as you can guess the rendering of the control to be able to display the actions around the tile. In the render function, you will find the code computing the positions and creating these actions. The computations are dynamic in order to handle the different number of actions. After the computation, it uses the CSS3 transform in order to rotate and translate the div created for the actions.
if(oControl.aTileOptions){// Start actions management
var startPoint, nbrLeft, firstPosition = 0; // initialize the variable
var rotationFactor = 51; // Fixed rotation factor (based on the action div size)
startPoint = -135; // Starting point of the rotations
if(oControl.aTileOptions.length%2 == 1){ // compute the number of elements to be placed on the left
nbrLeft = (parseInt(oControl.aTileOptions.length/2));
}else{
nbrLeft = (parseInt(oControl.aTileOptions.length/2) - 0.5);
}
// Compute the first position to be taken by the created action div.
firstPosition = startPoint - (nbrLeft*rotationFactor);
for(var i in oControl.aTileOptions){
var rotation = firstPosition + (rotationFactor*i); // Compute the rotation needed for the current action
var invertedRotation = rotation>0?-rotation:Math.abs(rotation); // compute the inverse rotation (used for the content)
rm.write("<div");// Start action container
rm.writeAttribute("id", oControl.getId() + "-action_"+i);
rm.writeAttribute("style", "transform: rotate("+ rotation +"deg) translate(80px,80px);"); // apply transformation
rm.addClass("actionItem");
rm.addClass("actionHidden");
if(oControl.aTileOptions[i].cssClass) // apply the custom or standard CSS class
rm.addClass(oControl.aTileOptions[i].cssClass);
else
rm.addClass("defaultActionClass");
rm.writeClasses();
if (oControl.aTileOptions[i].title) { // Write the action title as tooltip
rm.write(" title=\"");
rm.writeEscaped(oControl.aTileOptions[i].title);
rm.write("\"");
}
rm.write(">");
// Start Icon
rm.write("<div");
rm.addClass("innerActionItem"); // create the content of the action
// apply the inverted rotation to the content of the action div
rm.write(" style=\"color:" + oControl.aTileOptions[i].iconColor + "; transform: rotate("+ invertedRotation +"deg);\"");
rm.writeClasses();
rm.write(">");
var action_icon = oControl.getActionImage(oControl.aTileOptions[i].icon);// get the icon
action_icon.addStyleClass('roundedActionTileActionIcon');
rm.renderControl(action_icon);
rm.write("</div>");
// End Icon
rm.write("</div>");// End action container
oControl.aTileOptions[i].sId = oControl.getId() + "-action_"+i;// add the id of the created div in the aTileOption attribute for future reference
}
As you might have notice, I save the DOM id of the div created for the actions in the aTileOptions array. This is necessary to easily lookup the elements in order to hide and show them. This will be performed by the following function:
showActions:function(bVisible){
for(var i in this.aTileOptions){
var elem = this.$().find("#"+this.aTileOptions[i].sId);// lookup the element by id saved during the rendering
if(elem){
bVisible?elem.removeClass("actionHidden"):elem.addClass("actionHidden");// hide or show the DOM element
}
}
}
This function is not called by the control itself but by your controller to give a maximum of flexibility to your solution.
Having done all this, we just need to attach the events on the actions, and of course, a blur event to the control to hide the actions when the tile looses the focus… This is done after the rendering by the following function:
onAfterRendering:function(){
// Call the parent method as it's the one placing the tiles in the container
sap.m.Tile.prototype.onAfterRendering.call(this);
for(var i in this.aTileOptions){
// lookup the elements in order to add the listener on the click
var elem = this.$().find("#"+this.aTileOptions[i].sId);
if(elem){
elem.click(function(oEvent){
this.actionClicked({tile: this, actionId:oEvent.currentTarget.id});
}.bind(this));
}
}
// attach the blur event on the tile to hide the actions when it looses the focus
this.$().blur(function(){
for(var i in this.aTileOptions){
var elem = this.$().find("#"+this.aTileOptions[i].sId);
if(elem){
elem.addClass("actionHidden");
}
}
}.bind(this));
}
Now we just need to trigger the actionPress event declared in the control metadata and we will have a control ready to use. The actionPress event declared in the control metadata will be fired by the function actionClicked used by the onClick of the action. The JSON object used as parameter will contain the actionTag that you will have associated to the action definition… See point 2 bellow.
actionClicked:function(oParams){
for(var i in oParams.tile.aTileOptions){
if(oParams.tile.aTileOptions[i].sId === oParams.actionId){
oParams.action = { actionTag : oParams.tile.aTileOptions[i].actionTag};
break;
}
}
this.fireActionPress(oParams);
}
1.2) The CSS file
The CSS file used here is pretty straightforward as it’s all CSS3 classes used for the control rendering.
Please note the “defaultActionClass” which is the class used by default if you don’t specify any for the tiles or for the tile actions.
.defaultActionClass{
color: #007cc0;
border: 1px solid #007cc0;
background-color: rgb(255, 255, 255);
}
2) Using the control
2.1) The view
The roundedActionTiles are meant to be placed in a standard TileContainer (sap.m.TileContainer) that you need to declare in the view.
this.oTilesContainer =new sap.m.TileContainer();
this.oTilesContainer.setHeight("100%");
this.oTilesContainer.setVisible(true);
return new sap.m.Page({
title : "Rounded Actions Tiles",
enableScrolling : false,
content : [
this.oTilesContainer
]
});
2.2) The controller
In the controller, load the control using the “require”
jQuery.sap.require("controls.roundedActionTile.RoundedActionTile");
Assuming that you have declared (or loaded from your backend) a structure as follow:
tiles : [
{
title : "Tile 1",
icon : "Chart-Tree-Map",
options : [
{
title : "Add",
icon : "add",
cssClass: "exampleClass1",
actionTag: "Tile 1 - Action 1"
}, {
title : "Browse",
icon : "search",
actionTag: "Tile 1 - Action 2"
}
]
},
...
You just need to create the controls as:
for ( var c in this.tiles) {
var tileItem = new controls.RoundedActionTile();
tileItem.setTitle(this.tiles[c]["title"]);
tileItem.setIcon("sap-icon://" + this.tiles[c]["icon"]);
if (this.tiles[c]["cssClass"])
tileItem.setCssClass(this.tiles[c]["cssClass"]);
if (this.tiles[c]["iconColor"])
tileItem.setIconColor(this.tiles[c]["iconColor"]);
if (this.tiles[c]["options"]) {
tileItem.setTileOptions(this.tiles[c]["options"]);
}
this.tiles[c].object = tileItem;
tileItem.attachPress({
index : c
}, this.showOptions, this);
tileItem.attachActionPress(this.handleAction, this);
this.getView().oTilesContainer.addTile(tileItem);
}
and the rounded tiles with actions will be created.
Of course in your controller you need a method to handle the click on tiles to show the actions or, if no action has been defined on the tile, do what you need to do upon clicking on a tile… This function will use the “showActions” function defined on the control (as described in 1.1)
showOptions : function(oEvent, oParams) {
//Hide the visible actions (if any)
for ( var c in this.tiles) {
this.tiles[c].object.showActions(false);
}
// if the tile has actions, show them, else handle the "normal tile click"
if(this.tiles[oParams.index]["options"])
this.tiles[oParams.index].object.showActions(true);
else
this.handleAction(oEvent, oParams);
}
And then, last part of the code is to do the actual processing (navigate to a view, …) upon clicking an action or a tile without action
handleAction : function(oEvent, oParams) {
if(oEvent.getParameter('action'))
alert(oEvent.getParameter('action').actionTag);
else
alert(oEvent.getParameter('id'));
}
I hope you will enjoy this control, let me know your thoughts and comments 🙂
Happy coding 😉
Link to the GitHub repository: dafooz/roundedActionTilesUI5 · GitHub
Link to the JSFiddle for a live demo: Rounded Actions Tile in UI5 – JSFiddle (Note that the code has been slightly modified in the fiddle in order to have all the Javascript inline)
Awesome thought, i like the idea of having menus around the tile.
I have one question though will this menus static or visible only when you hover on the round tile.
Again great job.
Hi Kedar,
Thanks a lot!
The actions will appear upon clicking on the tile. I did this way to be consistent with the way the standard tiles are working. And in mobile, there's no such thing as hover so I guess it's the smartest approach... If you want static actions, you can easily achieve it with a minimum of changes in the control. Let me know if you want more details about this, I'll be happy to guide you.
Same functionality, better user experience 😉
this is a wonderful post Jonathan. Thank you for sharing here.
Thank you 🙂
Hi Jonathan,
I'm working on smiliar requirement where i want one big tile, when this tile is clicked 5 baby tiles should appear. Here I'm not able to change color property of each baby tile and put a title (small tiles would be bigger here too as i have to put title and icon both inside.)
Please Help!!!
Regards,
Shalini Mathur
Hi Shalini,
Can you try to describe exactly what you are trying to achieve?
When you say big tile, are you trying to display a rounded tile with the dimensions given here?
For the "baby tiles", will they be appearing using the same code as described here? Do you just need them to be bigger?
I'll be happy to help you once you give me more details...
In the meantime, if you are using my code and trying to change to colour of the subtiles, you can do it by giving a css class to the subtile.
For example, if I define the following css class
.exampleClass1{
color: red !important;
border: 1px solid green !important;
background-color: #dedede !important;
}
and, during the definition of the tile with subtiles I give that class (see code below)
{
title : "Tile 1",
icon : "Chart-Tree-Map",
options : [
{
title : "Add",
icon : "add",
cssClass: "exampleClass1",
actionTag: "Tile 1 - Action 1"
}, {
title : "Browse",
icon : "search",
actionTag: "Tile 1 - Action 2"
}
]
}
I will get a display with the first subtile colored while the second one will use the "standard" colours, as displayed in the screenshot bellow.
Thanks Jonathan for the blog.
Jonathan, how can I use this in XML view
solved.
Sorry for the late answer...
I'm glad you were able to adapt it to be able to use it in an XML view 🙂