Development of a UI Designer page as done at Bonitasoft
In this article, I will provide a step-by-step explanation, with examples, of how user interface pages are developed by the Bonitasoft team. Our work is aimed at replacing some of the existing pages of the current Bonita Portal.
Since we work in agile, our design choices are not set in stone - we update when we decide it’s appropriate to do so, and then apply the changes to all the pages. But, generally speaking, the choices explained here are ones that apply to pretty much every page that we create.
And before I start, I’ll note that when you are going to use the 2021.1 version of the Bonita development suite, you can import the pages we’re developing from the open source web pages project. They can be used as examples, and customized to your own needs.
Before starting development
When we start developing a page, we first create a mockup of what we want the finished page to look like.
This is mostly done in Paint.NET, and sometimes in Balsamiq, the Bonita UI Designer, or even using pen and paper. The benefits of specifying the page needs this way, before the development starts, fully outweigh the cost of the time and effort it takes to do this. For one thing, it lets us discuss the feasibility of new features. We have developed two types of pages that cover most use cases - list and details - so we choose the appropriate page type (skeleton) at the start.
General UI Designer standards
There are three standards that we apply with each new page that are useful to know before we proceed.
First, we always version our pages with a V*
suffix and update the version after each release. This way, someone that is external to Bonitasoft can import any page and modify it as they please, knowing that it won’t be overwritten when they upgrade to another version of the product.
Next, it’s useful to understand the way we work with variables in the Bonita UI Designer. We always try to have four variables per feature:
- The external API to retrieve the data
- A JavaScript variable that will hold the data - for example, the filter values or actions to be made
- A controller JavaScript variable containing utility functions or those that change the state of the data
- An event handler JavaScript variable. This listens to the data and prompts action depending on changes in it
And then, there are the page offsets. Depending on the size of the screen, we use an offset of 0/1/1/2 for xs/sm/md/lg screen sizes. These were chosen for two reasons:
The first is the look and feel of the application. Our application uses AngularJS pages in addition to the UI Designer pages that we are developing. We wanted to keep a consistent look and feel between the two types of pages without having to change much in the AngularJS pages.
The second reason comes from experimentation and is a subjective one. We tried 1/1/2/3 and 0/1/2/3 sizes, but neither of these looked as good to us as our current choice.
Now I’ll illustrate the page design process, using the Groups page as an example.
Overview of the Groups page
The Groups page is useful to integrate into an admin application, as its purpose is to show the groups of people in an organization.
Some examples of groups might be the “North America” group or the “Research & Development” group. Each group will have a number of users associated with it, and might have sub-groups.
For example, the North America group might have a Research & Development sub-group, and Walter Bates could be a member of that sub-group. (The mockup for this page, shown below, was done with the help of Paint.NET.).
As mentioned above, we usually start by creating the skeleton of the page in Bonita UI Designer. Since this is a page of the type list, we will get a collection of items from the API call and then use the repeatable container to display the information for each item.
Getting the information in the Bonita UI Designer is easy and straightforward. We create an external API variable, called groupsUrl
with the value ../API/identity/group?c=10&p=0&o=displayName ASC
.
This is everything needed to get the information from the API and save it in the groupsUrl
variable.
Displaying the list of groups
Now of course we want to display this information.
As mentioned above, our team usually uses page offsets. To apply the aforementioned offsets, the current solution is to drag and drop a container, change its size to 1, add a second container next to it, change its size to 10, and then add a third container to its right. This will create offsets of 1 for the page. Next, go through each one of the xs/sm/md/lg sizes and change the left and right containers accordingly.
For the data, we can drop a container in the middle container and use groupsUrl
as a value in the collection property of the new container. For each item in the list to have a delimiter, we can add group-item
as a CSS class for this container.
For the fields to display, from the mock file, we see from the mockup that we want to show the data for Display name, Name, Parent group, Created on and Updated on. We also want the user to see the description at the bottom of each item.
Since we will also have some buttons to the right, we start by adding a container that has size 9 inside the repeatable container. We can drop an additional container to the right of it in which we will put the buttons later.
For readability purposes, we usually put each field in a separate container.
The sizes for each field is for you to define; we went with 2, 2, 3, 2, 3 because we thought it looked good on the page. In each container, we added two text widgets: the first acts as a label, and the second holds the value. These also have item-label
and item-value
CSS classes respectively so they are styled accordingly.
The item labels are taken from the mockup image. They can be simply written as the text widget values. For the item values, we usually look at the API call to know which json object property to display, and we use the standard angularjs interpolation.
For example, since we want to display the name of a group, we use {{$item.name}}
.
There are three special cases that don’t follow this “item-label/item-value” formula.
- The first is when trying to display a date. Even though dates have the
item-label/item-value
CSS classes, the values need to use a specific formatting. This is done by using the uiDate AngularJs directive, so the value of theitem-value
text widget will be{{$item.creation_date.replace(" ", "T") | uiDate:'short'}}
, which formats the creation date here with the short style. - The second specific case is the description. As we can see on the mockup, there is no item label, and the third and fourth group have no description. To create this we add two text fields with the
item-label
CSS class. The first text field has the value{{$item.description}}
, and the second one hasNo description
. We can now use the hidden property on each of the text widgets to display depending if the description property is empty. - The third specific case is the parent group. As we can see on the mockup for the first group, the parent group value is
--
. We will need a function to know if there is no parent group. As I said earlier, our team usually puts this kind of function in a controller variable. So, we create the groupsCtrl variable with the value: return { isInformationEmpty(information) { if (!information) { return "--"; } return information; }}; We can then use this function in the item value field for the parent group.
Another important thing to note about this parent group is that it has the potential to have a really long value. We use a tooltip to display the entire value, so the value for this field would be something like this:
<div title="{{groupsCtrl.isInformationEmpty($item.parent_path)}}">{{groupsCtrl.isInformationEmpty($item.parent_path)}}</div>
To style everything, we went with this CSS, which not only creates a delimiter between each item, but also creates specific styles for the item values and the item labels. The last thing it does is create a hover effect on each item.
.group-item {
border-bottom: 1px solid lightgray;
overflow: hidden;
transition: 0.3s;
padding-top: 0.5rem;
}
.group-item p {
margin-bottom: 0;
}
.group-item:hover {
box-shadow: 0 0 5px;
margin-top: 0px;
}
.item-label p,.item-label.component {
font-size: 11px;
opacity: 0.7;
margin: 0;
padding-right: 0;
word-wrap: break-word;
word-break: break-word;
}
.item-value,.item-value a,.item-value p {
font-size: 12px;
margin: 0 0 3px 0;
padding-right: 0;
word-wrap: break-word;
word-break: break-word;
}
In the end, it looks something like this in the Bonita UI Designer whiteboard for a lg screen.
With something like this in the preview.
After displaying the list of groups, what remains to do are the filters, the load more items button and the action buttons.
There are also still some small improvements to do to the page itself, like adding a title, showing the number of displayed items for when we will use load more and also adding a message to display when there are no groups available.
We add the buttons first, and will implement them later. For the three small improvements, they are pretty straightforward, so I will not go into detail here. Just keep in mind that after we finish the load more button, there will be some small changes to when we display the number of items shown and the no more groups message.
Filters
Let’s start with the filters and specifically, with the data that we need to create for the filters. We can add a new Javascript variable, groupsData
, that will contain the following:
return {
sortByOptions: [
{ displayKey: "Display name (Asc)", returnKey: "displayName+ASC" },
{ displayKey: "Display name (Desc)", returnKey: "displayName+DESC" },
{ displayKey: "Name (Asc)", returnKey: "name+ASC" },
{ displayKey: "Name (Desc)", returnKey: "name+DESC" }
],
filters: {
sortOrder: "",
searchValue: ""
}
};
The sortByOptions
array contains the “sort by” possibilities. As you can see, each object contains a displayKey
and a returnKey
. The first is what the user will see, and the second is what will return from the select box when the user chooses the value.
The filters
object contains the values of the sort select box and the search input. We usually add a container for the filters and a CSS class to leave a small space between the title and the filters. With this data created, we can now add the two filters. To take into account the values of the filters, we create a function that will use the filter values. We also need to use this function in the external API.
Add the following to the groupsCtrl
variable:
groupsQueryParameters: function() {
var queryParameters = "c=10&p=0”;
if ($data.groupsData.filters.sortOrder) {
queryParameters += "&o=" + $data.groupsData.filters.sortOrder;
} else {
queryParameters += "&o=displayName ASC";
}
if ($data.groupsData.filters.searchValue) {
queryParameters += "&s=" + $data.groupsData.filters.searchValue;
}
return queryParameters;
}
We can then use the function in the external API, with the new value for groupsUrl
:
../API/identity/group?{{groupsCtrl.groupsQueryParameters()}}
Load more items button
The current implementation of the load more button follows a specific algorithm. When the page is loaded, there is an API call for 20 items (2 pages of groups). If there are more than 20 groups, we will get 10 items (the third page of groups) as a result of the button click, but the second page from the first API call is displayed. Clicking the button again, we will do a third API call to get 10 items and display the 10 items of the second API call.
To know when to disable the button, we need to be able to display a page from memory and get the next page of items from the API. When there are no more results in the API, we will know that the button should be disabled. To implement this load more button, we found that we need at least 4 variables.
We also created 2 variables that we found useful, but are not absolutely necessary.
For the 4 variables that are necessary:
- An array that will keep the groups that are displayed currently
- An array with the result from the API for page 3 and after
- An array that keeps the next results to concatenate to the current displayed ones
- A variable that keeps the current page
And, the 2 other variables that are useful are:
- One that keeps the number of groups to request per page in case we want to switch to another number
- One that saves the number of groups that we will have for the page in the memory
So, in the end, our groupsData
will look like this:
return {
groupsInMemory: [],
pageId: 0,
groupsFromAPI: undefined,
displayedGroups: [],
numberOfGroupsPerPage: 10,
numberOfgroupsInMemory: 0,
sortByOptions: [
{ displayKey: "Display name (Asc)", returnKey: "displayName+ASC" },
{ displayKey: "Display name (Desc)", returnKey: "displayName+DESC" },
{ displayKey: "Name (Asc)", returnKey: "name+ASC" },
{ displayKey: "Name (Desc)", returnKey: "name+DESC" }
],
filters: {
sortOrder: "",
searchValue: ""
}
};
The implementation
Let’s start by setting up the API calls. To keep them generic, we modify the query parameters function in the controller by passing the page number.
At the start of the page it will be 0 and we should get 20 elements. After the first call, we pass a page number to the query parameters function.
The function in the end should look something like this:
groupsQueryParameters: function(pageNumber) {
var groupsPerPage = $data.groupsData.numberOfGroupsPerPage;
if (pageNumber === 0) {
//on first call we need to load two pages
groupsPerPage *= 2;
}
var queryParameters = "c=" + groupsPerPage + "&p=" + pageNumber;
if ($data.groupsData.filters.sortOrder) {
queryParameters += "&o=" + $data.groupsData.filters.sortOrder;
} else {
queryParameters += "&o=displayName ASC";
}
if ($data.groupsData.filters.searchValue) {
queryParameters += "&s=" + $data.groupsData.filters.searchValue;
}
return queryParameters;
}
We need to consider two more things: the load more button itself and the event handler.
The load more button should do an API call to get the groups after the first page, thus it will have
../API/identity/group?{{groupsCtrl.groupsQueryParameters(groupsData.pageId)}}
as the url to call.
The successful response will be stored in the groupsData.groupsFromAPI
array. The button should be disabled when the number of groups displayed cannot be divided by the number of groups per page. For example, if there are 24 elements, there will be 4 groups left after the second page is shown.There is no fourth page. So after showing the third, we can disable the button. We also disable the button if there are no groups in the memory.
So, the condition is:
groupsData.displayedGroups.length % groupsData.numberOfGroupsPerPage > 0 || groupsData.numberOfgroupsInMemory === 0
We also want to hide the button if the displayedGroup
array is empty, so we need to take into account if there are any groups to display. So, for the “groups shown” and the “no group to display” fields, we need to verify if the displayedGroup
array contains data.
To complete the list feature, we configure the load more handler.
There are three different data states which trigger it:
- When the user first arrives on the page
- When the user clicks on the load more button and there is another page to display
- When the user clicks on the button and there are no more pages, but there is still something in the memory
Let’s tackle each one of these separately.
After we get the first 2 pages from the API, we save the first page in the displayedGroup
array and save the second page in the memory in groupsInMemory
. We also set the number of groups for the next page to be equal to the size of groupsInMemory
. Then we set the pageId to 2 in order to get the third page the next time we do the API call, and to set the groupsUrl
to “empty” in order to do all of this only once. During the execution, we don’t need to reuse the groupsUrl
.
The second part of the load more handler should add the page from memory to the displayed list of groups, and add the page returned by the API to the memory. We should also increment the pageId so the next API call will request the next page. Lastly, we make the result from the API request empty so this gets executed only once.
The third part of the load more handler adds the results from the memory, and then cleans those results from the memory, so we don’t execute the case multiple times.
In the end, the handler looks something like this:
if($data.groupsUrl) {
$data.groupsData.displayedGroups = $data.groupsUrl.slice(0, $data.groupsData.numberOfGroupsPerPage);
$data.groupsData.pageId = 2;
$data.groupsData.groupsInMemory = $data.groupsUrl.slice($data.groupsData.numberOfGroupsPerPage);
$data.groupsData.numberOfGroupsInMemory = $data.groupsData.groupsInMemory.length;
$data.groupsUrl = undefined;}if($data.groupsData.groupsFromAPI && $data.groupsData.groupsFromAPI.length) {
$data.groupsData.displayedGroups = $data.groupsData.displayedGroups.concat($data.groupsData.groupsInMemory);
$data.groupsData.pageId++;
$data.groupsData.groupsInMemory = $data.groupsData.groupsFromAPI;
$data.groupsData.numberOfGroupsInMemory = $data.groupsData.groupsInMemory.length;
$data.groupsData.groupsFromAPI = undefined;
}
if($data.groupsData.numberOfGroupsInMemory > 0 && $data.groupsData.groupsFromAPI && $data.groupsData.groupsFromAPI.length === 0) {
$data.groupsData.displayedGroups = $data.groupsData.displayedGroups.concat($data.groupsData.groupsInMemory);
$data.groupsData.groupsInMemory = undefined;
$data.groupsData.numberOfGroupsInMemory = 0;
}
The end product looks something like this in the Bonita UI Designer whiteboard:
And something like this in the preview:
Conclusion
You now know how to develop a list styled page as us at Bonitasoft do it!
And of course the next thing to do - which is very important - is to test the page. This article does not go into that, but you can find out more about how to test a Bonita UI Designer page using Cypress in this article. You can find the page I’ve described here, as well as others that were developed and tested, in the web-pages Github repository.
Thank you for reading and stay tuned for the next article about how the R&D at Bonitasoft develops the modals, which will be useful for modifying the list of groups.