Wednesday, January 03, 2024

UI5 Tips: Persisting JSONModel data using browser Storage

In this ui5tip, we'll take a look at integrating ui5's sap.ui.model.json.JSONModel with sap.ui.util.Storage utility. Our immediate use case for this was to allow easy and transparent persistence of UI State, which has its own dedicated tip. In this tip, we describe how the actual persistence is implemented.

Sample application: a Shopping List

To illustrate just the storage model, we developed a tiny Shopping List application. You can run it yourself by downloading the contents of the localstoragemodel folder and exposing them with your webserver. This is what the application looks like: Screenshot of the Shopping List sample application to illustrate the local storage model.

Sample Application Features

  • A Products list (left), and a Shopping list (right). Users can browse the Products list and see name and price. The Products list has a row action button with a shopping cart icon. If the product is already on the shopping list, the shopping cart appears full. Hitting the row action button will add the product to the Shopping List.
  • In the Shopping List, users see the product name, item price, quantity, and item total. The items in the shopping list also have a row action to remove the item from the shopping list.
The Shopping List also has a toolbar with some buttons that control the Shopping List Data:
  • The Save button will save the current contents of the shopping list to the local storage
  • The Undo button will restore the current contents of the shopping list with whatever data was stored in the local storage
  • The Submit button represents the action of actually placing an order for the shopping list. It will also clear the shopping list and save.
  • The Clear button will empty the current contents of the shopping list, but without saving the state to the local storage.

Sample Application Demo

To test the application, try the following sequence of actions:
  1. Open the application. Initially, the Shopping list should be empty.
  2. In the Products list, add a Product to the shopping list by hitting the shopping cart button. The item should be added to the Shopping list.
  3. Refresh the browser window. When the application reloads, you'll notice that the shopping list is empty - that's expected, since you didn't save the shopping list.
  4. Now, repeat step 2 and add some products to the shopping list. Hit the Save button.
  5. Refresh the Browser again. Now, when the application reloads, the products you added in step 4. should re-appear automatically in the list.
This demonstrates that the application is capable of persisting the saved shopping list data. Instead of refreshing the window, you can also try to completely close the browser, or even reboot your machine. But when you revisit the application - with the same browser - then you'll notice that the data will still appear. In addition to the persistence the application also provides a simple, one level undo action. Whenever you make a modification to the list, either by adding a new item, removing an item, or modifying the quantity of an item, both the Save and the Undo button will become enabled. We already demonstrated the Save button action. Hitting the Undo action button will restore the contents of the shopping list with whatever was available in the Storage, restoring the contents of the list to the previously saved state. You can use your browser to inspect the local storage. It might look something like this: Screenshot of the Shopping List sample application to illustrate the local storage model. In the remainder of this tip we will discuss how these features were built by combinding two classes in the ui5 framework - the sap.ui.util.Storage utility and the sap.ui.model.json.JSONModel.

sap.ui.util.Storage utility

The sap.ui.util.Storage utility offers a UI5 APIto access the Browser's standard HTML5 Web Storage API. It's a pretty basic, no-nonsense wrapper for managing modest amounts of data based on key/value access. Of course, you can use the sap.ui.util.Storage utility directly and code your own logic to control exactly when you want to retrieve and store some data. While there is nothing against that approach, we envisioned something that also works when using models and data binding. This may need a little bit of explanation.

UI5 models

Models are a way to achieve managed data access. A model manages a particular collection of data, and can be shared across multiple elements of the application, or even be accessible to all elements of the application. For example, both the Product List and the Shopping List are each managed by their own model, which are declared in the application's manifest.json:
  ...,
  "models": {
    "products": {
      "type": "sap.ui.model.json.JSONModel",
      "dataSource": "products"
    },
    "shoppingList": {
      "type": "ui5tips.utils.LocalStorageJSONModel",
      "dataSource": "shoppingListTemplate"
    }
  },
  ...
The model can be observed by listening to its events, and this allows different parts of the application to react whenever something interesting happens to the state of the model - i.e. when its data is manipulated. For example, in MainPage.controller.js, an event handler is attached to listen to the Shopping list's dirtyStateChange and propertyChange events, which in turn control certain aspects of the screen logic, such as enabling and disabling the Save and Undo buttons:
  ...,
  initShoppingListModelHandlers: function(){
    var shoppingListModel = this.getShoppingListModel();
    shoppingListModel.attachDirtyStateChange(function(event){
      this.dirtyStateChanged(event.getParameters());
    }, this);
    shoppingListModel.attachPropertyChange(function(event){
      var path = event.getParameter('path');
      var context = event.getParameter('context');
      var itemsPath = '/items';

      if (path === itemsPath || context && context.getPath() === itemsPath) {
        this.itemsChanged(shoppingListModel.getProperty(itemsPath));
      }          
    }, this);
  },
  ...

Data Binding

In Ui5, databinding is a mechanism that lets you declaratively construct objects and change their properties based on the state of a model. The declarative aspect means that no explicit coding is involved. For example, rather than setting up an event handler that contains explicit code to respond to changes to the state of the model, you can use a special syntax in designtime property assignments that ensures the runtime property value will be assigned directly from some part of the data in the model. Some examples include:
  • The actual data in both the Product List and Shopping List. Both these are implemented using a sap.ui.table.Table control, which only support adding rows through data binding. For example, take a look at ShoppingList.fragment.xml to see how it gets its rows from the Shopping List model:
  ...
  <table:Table
    id="shoppingList"      
    title="Shopping List"
    editable="true"
    selectionMode="None"
    enableBusyIndicator="true"
    visibleRowCountMode="Auto"
    rowActionCount="1"
    rows="{
      path: 'shoppingList>/items'
    }"
  >
  ...
(This example basically says: create a row in the shopping list for each item in the shopping list model)
  • The Product List's row action shows a full or empty shopping cart, depending upon whether the product is already in the shopping list. This is achieved in Products.fragment.xml with databinding, which passes the current product and the items from the shopping list to the controller's getShoppingCartRowActionIconSource formatter function:
  ...
  <table:RowActionItem
    binding="{shoppingList>/items}"
    icon="{
      parts: [
        {path: 'products>'},
        {path: 'shoppingList>'}
      ],
      formatter: '.getShoppingCartRowActionIconSource'
    }"
    text="Add to Cart"
    press="onCartButtonPressed"
  />
  ...
(This example says: call the getShoppingCartRowActionIconSource method to obtain an icon, depending on the current product from the product model and all the items in the shopping list model.)
  • In ShoppingList.fragment.xml, the state of the Submit and Clear buttons is enabled depending upon whether the shopping list has any items:
  ...
  <m:contentMiddle>
    <m:Button 
      id="approvalButton"
      icon="sap-icon://cart-approval"
      tooltip="Send Order"
      enabled="{= ${shoppingList>/items}.length > 0 && ${shoppingList>/items/0} !== undefined }"
      press="onApproveButtonPressed"
    />
  </m:contentMiddle>
  <m:contentRight>
    <m:Button 
      id="clearAllButton"
      icon="sap-icon://clear-all"
      tooltip="Clear Shoppinglist"
      enabled="{= ${shoppingList>/items}.length > 0 && ${shoppingList>/items/0} !== undefined}"
      press="onClearButtonPressed"
    />
  </m:contentRight>
  ...
(This example says uses a slightly different binding syntax called expression binding to enable the button if there is at least one item in the shopping list.) While all these features could also have been implemented by explicit coding, data binding allows a lot of this to be defined completely declaratively in the view, with much less code, and denoted in a way that transparently and unambiguously ties the data to the relevant item in the UI. Now - there is no shame (I think) in not immediately embracing ui5's data binding. There are a number of areas that can be somewhat complex and unintuitive at first. But by using it more and more often, you start experiencing the benefits, and - just as important -, learn about the limitations. Now, this post is not an in-depth article on ui5 databinding. It's just that, at some point, you learn to use it in such a way that it becomes one of the most important factors in how you design ui5 applications, as well as the way different parts of the application communicate with each other. So, we consider using ui5 models and data binding as a given. And if you find you have a need for the kind of client-side persistence capabilities offered by the Web Storage API, then you are probably not interested in that as an isolated way of storing some bits of data. Instead, you're going to want to have a normal, regular ui5 model that incorporates these persistence features.

A sap.ui.model.json.JSONModel backed by sap.ui.util.Storage

We decided to take the sap.ui.model.json.JSONModel as a base, and extend it to add a few methods that allow the model's data to be stored and retrieved from sap.ui.util.Storage. The reason for this approach is to allow our model exactly the same as the standard ui5 sap.ui.model.json.JSONModel. This means that in particular, all behavior with regards to databinding will be exactly as with the sap.ui.model.json.JSONModel. In theory, it would also be possible to extend the abstract sap.ui.model.ClientModel, but it turns out that implementing reliable databinding is not as easy as it seems. Or I should say, I took a naive shot at doing that, and failed. While it might be very instructive to try it in earnest, I decided that at this point I am more interested in having a working solution than to learn all the ui5 internals required to succesfully implement databinding. The result is the LocalStorageJSONModel.

Instantiating the LocalStorageJSONModel from the manifest.json

The sample application creates the LocalStorageJSONModel implicitly by declaring it in the manifest.json:
      "shoppingList": {
        "type": "ui5tips.utils.LocalStorageJSONModel",
        "dataSource": "shoppingListTemplate"
      }
It gets initialized with a datasource called shoppingListTemplate which is also declared in the manifest.json:
      "shoppingListTemplate": {
        "uri": "data/shoppingListTemplate.json",
        "type": "JSON"
      }
The datasource refers to some configuration data stored in data/shoppingListTemplate.json and its contents are:
{
  "autoSaveTimeout": -1,
  "storagePrefix": "shoppingList",
  "template": {
    "items": [
    ]
  }
}
This data is passed as first argument to the LocalStorageJSONModel constructor. Its properties are:
  • int autoSaveTimeout: (optional) an integer specifying the number of milliseconds to wait after the last change to the model before automatically saving the model's data to the persistent storage. If this is 0 or less, data is not automatically persisted.
  • string storagePrefix: (optional) a string that is used to prefix the key under which the models data will be stored in storage. The sap.ui.util.Storage constructor takes a storagePrefix, and the LocalStorageJSONModel takes its own class name for that. But if you have several of these models in one application, you can keep them apart by specifying a specific storagePrefix here.
  • object template: (optional) an object that will be used as template data for the model.

Instantiating the LocalStorageJSONModel directly

Of course, you can also import the class into your in your ui5 classes (for example, in a controller) and call its constructor to create an instance:
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "ui5tips/utils/LocalStorageJSONModel"
], 
function(
  Controller,
  LocalStorageJSONModel
){
  "use strict";  
  var controller = Controller.extend("ui5tips.components.app.App", {
    onInit: function(){
      var localStorageModel = new LocalStorageJSONModel({
        "autoSaveTimeout": -1,
        "storagePrefix": "myApp",
        "template": {
          ...data...
        }
      });
      this.getView().setModel(localStorageModel, 'localStorageModel');
    }
  });
  return controller;
});

Key Methods

The most important methods provided by LocalStorageJSONModel are:
  • loadFromStorage(template): populates the model with the data persisted in the storage. If the template argument is specified, then the data from the storage is patched with the data in the template. (For more details, see the next section about model initialization and the template). In the sample application, the Undo button action is implemented by calling loadFromStorage():
    onUndoButtonPressed: function(){
      var shoppingListModel = this.getShoppingListModel();
      shoppingListModel.loadFromStorage();
    }
  • saveToStorage(): stores the model data to the browser storage. In the sample application, the Save button action is implemented by calling the saveToStorage() method.
    onSaveButtonPressed: function(){
      var shoppingListModel = this.getShoppingListModel();
      shoppingListModel.saveToStorage();
    }
  • deleteFromStorage(): permanently removes the data from the local storage. Use this if you're sure the application will not need any of the currently stored data anymore.
  • isDirty(): returns a boolean that indicates whether the current model state is different from what is stored. If it returns true, it means the current state of the model is different from the stored state. Note that you can also use the dirtyStateChanged event to get notified of a change in the dirty state.

Template and Model initialization

As part of model initialization, whatever data the browser had associated with the storagePrefix is retrieved. If a template is specified, then the data retrieved from the storage is patched with the template and the resulting data structure is immediately saved to the storage. This provide a basic method to evolve the structure of the model and pre-populate it with any defaults. The patching of the data occurs non-destructively: only those paths in the template that do not exist already in the stored data structure will be added. If you need it, you can always apply more advanced patching schemes after instantiation, but in many cases, this built-in behavior will suffice to update and upgrade the model structure as your application grows and gets more features. You can use the following methods to work with the template:
  • getTemplateData(): retrieve the template passed to the constructor.
  • resetToTemplate(): repopulates the model with the template. Any data stored in the model will be lost.
  • updateDataFromTemplate(data, template): utility method that is used to patch the data argument with the template argument. It returns a object that represents the merge of the data argument and the template argument.

Events

The LocalStorageJSONModel provides these events:
  • dirtyStateChange: this event has two parameters, isDirty to indicate whether the model is now dirty and wasDirty, indicating whether the model was dirty prior to the latest change. The sample application uses this event to determine whether to enable or disable the Save and Undo buttons:
    shoppingListModel.attachDirtyStateChange(function(event){
      this.dirtyStateChanged(event.getParameters());
    }, this);
and
    dirtyStateChanged: function(parameters){
      var isDirty = parameters.isDirty;
      this.byId('saveButton').setEnabled(isDirty);
      this.byId('undoButton').setEnabled(isDirty);
    },
The following events can be used to keep track of the model state:
  • attachDirtyStateChange(data, handler, listener): attach a handler function to get notifications of a change in the dirty state. If a some change is made that causes a difference between the stored data and the model data, this event is fired and the handler is called in the scope of the listener, and gets passed the application specific payload data.

Autosave

The sample application controls when the model data will be persisted to local storage by calling saveToStorage() explicitly. But there are also use cases where you simply want the storage to always reflect the state of the model, or at least, track it as closely as possible. The persistence of UI state is such a case, and for these scenarios the LocalStorageJSONModel supports an automatic save feature. Autosave works by monitoring the state of the model, and then saving to the storage whenever a change is detected. While saving to storage should generally be pretty fast, it is a blocking operation. So rather than always explicitly persisting after a change occurs, we simply buffer the change events with the bufferedEventHandler and persist the data to storage some time after the occurrence of the last change event. To use the autosave feature, simply pass a positive value for the autoSaveTimeout property when you instantiate the model. Alternatively, can also get or set the value of the autoSaveTimeout property after model construction by calling the getAutoSaveTimeout() and setAutoSaveTimeout() methods respectively. To disable autosave, simply set the property to a zero or negative value.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

UI5 Tips: Change expand/collapse icons for Tree, Panel and TreeTable using only CSS

UI5 offers a couple of widgets that can expand and collapse. To do that, these controls render a button with an icon that indicates the current state, and which the user can click to toggle the state. The standard icons that UI5 renders for the expand/collapse button are navigation arrows, which some of our users disliked. In this tip, you'll learn how you can replace them with more appropriate icons using only a few lines of CSS. No javascript code is involved. If you want to check out this tip yourself, download the app from the expandcollapse directory and expose it to your webserver. You can then navigate to index.html to see the sample app in effect.

UI5 exandable/collapsible Controls

First, lets take a look at the standard UI5 controls.

Panel

The sap.m.Panel has an expandable property. If true the Panel renders a button which the user can use to hide and show the contents of the panel. A screenshot is shown below: An expandable sap.m.Panel (This screenshot is taken from UI5's Panel - Expand / Collapse sample)

Tree

The sap.m.Tree is a classical way of presenting hierarchically organized items like a folder structure. A screenshot is shown below: A sap.m.Tree control (This screenshot is taken from UI5's Tree - Basic sample)

TreeTable

The sap.ui.table.TreeTable is just like a regular data grid table (sap.ui.table.Table), but with an added functionality to hierarchically organize the rows in the table, and with the ability to expand or collapse rows according to the hierarchy. A screenshot is shown below: A sap.ui.table.TreeTable (This screenshot is taken from UI5's sap.ui.table.TreeTable JSONTreeBinding sample)

A look at the icons

Let's take a look at the standard icons that UI5 renders for the expand/collapse button: UI5 Right arrow icon UI5 Down arrow icon

Proposed Icons

While I don't really have a problem with these icons, some of our users had a problem recognizing the collapse/expand functionality for Panels. We looked a bit around in the UI5 Icon explorer and decided we'd rather use these icons instead: UI5 Expand icon UI5 Expand icon Going by their name, it's a bit of a mystery to me why they weren't used by UI5 in the first place. But anway, now we have this tip to explain how you can change them.

CSS to change the icons

We prepared a separate CSS file for each of the aforementioned UI5 controls, and included them into the app via the manifest.json:
"resources": {
  "css": [
    { "uri": "css/ui5-customization-m.Panel.css" },
    { "uri": "css/ui5-customization-m.TabContainer.css" },
    { "uri": "css/ui5-customization-m.Tree.css" },
    { "uri": "css/ui5-customization-ui.tree.TreeTable.css" }
  ]
}

How UI5 renders icons

Before we discuss how to apply the CSS to change the icons, it's useful to understand how UI5 icon rendering works. In general, UI5 uses icon fonts. The UI5 framework loads a library.css stylesheet, which has a @font-face rule like this:
@font-face {
    font-family: "SAP-icons";
    src: url('../base/fonts/SAP-icons.woff2') format('woff2'),
         url('../base/fonts/SAP-icons.woff') format('woff'),
         url('../base/fonts/SAP-icons.ttf') format('truetype'),
         local('SAP-icons');
    font-weight: normal;
    font-style: normal
}
This binds the name SAP-icons to the font resource, and will ensure that whenever a HTML element is assigned the font-family: "SAP-icons" css property, it will render whatever text it contains with glyphs from that font. Now, when using the UI5 javascript API, you don't actually ever have to deal with these details at this level. Rather, if you ever need to assign an icon explicitly, for example, when using a sap.ui.core.Icon control, you can assign a custom icon uri using the sap-icon protocol, which maps more or less reasonable icon names to the glyph that depicts the desired icon. (You can read more about the sap-icon uri protocol in the Icon topic of the SAP UI5 walkthrough) Apart from these explicitly assigned icons, the renderer classes of various UI5 controls will write out the required HTML code for the icons that just happen to be fixed to it. Let's call these structural icons. For example, there is no property that allows you to change the icon that a sap.m.Panel uses for its exapand/collapse button - that's just part of how the Panel happens to be coded - it's part of its structure. As we will see in the following sections, the font-family is just the underlying medium that allows the UI5 framework to render icons. The details of how a particular control renderer renders its structural icons can still vary a bit, and we'll need to figure out how a particular control renders its icons before we can change them.

How sap.ui.table.TreeTable renders the collapse/expand icons

The sap.ui.table.TreeTable renderer takes a straightforward approach to rendering the collapse/expand icons. If you open one of the standard UI5 TreeTable samples, and right click the expand/collapse icon to inspect it (for example, with the Chrome developer tools), then you might see something like this: Inspecting the sap.ui.table.TreeTable expand/collapse icon with chrome developer tools The sap.ui.table.TreeTable renderer has written a <span> element with a sapUiTableTreeIcon class:
<span 
  class="
    sapUiTableTreeIcon 
    sapUiTableTreeIconNodeClosed
" 
  title="Expand Node" 
  role="button" 
  aria-expanded="false"
></span>
The span does not actually contain any text - rather a css ::before pseudo class is used for that. This is also used to bind it to the "SAP-icons" font, using the font-family property - this ensures that element will render glyphs from the icon font:
.sapUiTableTreeIcon::before {
    font-family: "SAP-icons";
    font-size: .75rem;
    color: #0854a0;
}
The actual text content that determines the icon is controlled through another rule, using another css class, which uses the css content property to write out the character that renders the appropriate icon from the font. When collapsed, its:
.sapUiTableTreeIcon.sapUiTableTreeIconNodeClosed::before {
    content: '\e066';
}
(You may recall that \e066 is the character that corresponds to the navigation-right-arrow icon.) When expanded, its:
.sapUiTableTreeIcon.sapUiTableTreeIconNodeOpen::before {
    content: '\e1e2';
}
(You may recall that \e1e2 is the character that corresponds to the navigation-down-arrow icon.) This way, the sap.ui.TreeTable only needs to change the style class from sapUiTableTreeIconNodeClosed to sapUiTableTreeIconNodeOpen on the <span>, depending on the expanded/collapsted state of the row: the css magic will take care of rendering the right icon.

Changing the expand/collapse icons for the sap.ui.table.TreeTable

As we have just witnessed, the sap.ui.table.TreeTable uses separate classes for the collapse and expand icons. This makes it really quite simple to change the icons. We only have to write our own rules for the sapUiTableTreeIconNodeOpen::before and sapUiTableTreeIconNodeClosed::before classes to mask the default ones, and assign the proper value for the content property:
/**
* sap.ui.table.TreeTable: better icons for expanded
*/
.sapUiTableTreeIcon.sapUiTableTreeIconNodeOpen::before {
  content: '\e1d9';
}
/**
* sap.ui.table.TreeTable: better icons for collapsed
*/
.sapUiTableTreeIcon.sapUiTableTreeIconNodeClosed::before {
  content: '\e1da';
}
(You will find similar rules in the ui5-customization-ui.tree.TreeTable.css provided by this ui5tip) The only thing we really need to think of when applying this stylesheet is that it is loaded after UI5 framework loads the CSS specific to the ui.tree.TreeTable control: if our CSS is loaded before the framework's CSS, then our rules will be masked by the framework's, and we want to do it exactly the other way around. To ensure that the framework's CSS for the ui.tree.TreeTable control is loaded before our custom CSS, simply include the sap.ui.table library in the data-sap-ui-libs property of the <script> element you use to load UI5. (see the index.html for this tip):
<script
  id="sap-ui-bootstrap"
  src="https://openui5.hana.ondemand.com/1.87.0/resources/sap-ui-core.js"
  data-sap-ui-theme="sap_belize"
  data-sap-ui-libs="sap.m, sap.ui.table"
  data-sap-ui-bindingSyntax="complex"
  data-sap-ui-compatVersion="edge"
  data-sap-ui-preload="async"
  data-sap-ui-resourceroots='{
    "ui5tips": "./"
  }'
></script>
That's it! The screenshot below shows what the TreeTable looks like in this tip's sample app: sap.ui.table.TreeTable with modified collapse/expand icons

How sap.m.Panel renders the collaps/expand icons

Let's take a look at how the sap.m.Panel renders its collapse/expand icon. We can again open UI5's own sap.m.Panel sample and use our browser's develpoment tools to inspect the page's HTML code: Inspecting sap.m.Panel's standard expand/collapse icon. Just like the sap.ui.table.TreeTable we discussed in the previous section, the sap.m.Panel renders a <span> element for the icon, which is assigned a CSS class to mark it as the icon, and wich is bound to the icon font face:
<span 
  data-sap-ui-icon-content=""
  class="
      sapUiIcon 
      sapUiIconMirrorInRTL 
      sapMBtnCustomIcon 
      sapMBtnIcon 
      sapMBtnIconLeft" 
  style="font-family: 'SAP\2dicons';"
></span>
And, just like for the sap.ui.table.TreeTable, there is a CSS rule to select the ::before pseudo-class, which has the content property to insert the appropriate character that corresponds to the glyph.
.sapUiIcon::before {
    content: attr(data-sap-ui-icon-content);
    speak: none;
    font-weight: normal;
    -webkit-font-smoothing: antialiased;
}
There are some remarkable differences too with respect to the sap.ui.tree.TreeTable example. In this case, there are no separate classes corresponding to the collapsed/expanded state of the Panel. Instead, the content property of the .sapUiIcon::before pseudo-class uses the value of the elements data-sap-ui-icon-content attribute. It will render whatever text is in the elements data-sap-ui-icon-content attribute. If you check the code for the <span>, you'll note the data-sap-ui-icon-content has been assigned some text, which is rendered as a so-called .notdef glyph, both in the developer tools and here on the page. (The .notdef glyph is the "boxed question mark"). You can copy the text from the data-sap-ui-icon-content attribute in the browser tools and paste it in a hex editor, or in a javascript string to figure out what its character code is, for example:
// decimal: 57839
"".charCodeAt(0)

// hex: 0xE1EF
("".charCodeAt(0)).toString(16)
Inspecting the value of the data-sap-ui-icon-content attribute It turns out that this corresponds to UI5's slim-arrow-down icon, which has a similar appearance to the down-arrow icon. If you collapse the panel and inspect it again, you'll notice that the value of the data-sap-ui-icon-content is now 0xE1ED, which corresponds to UI5's slim-arrow-right icon.

Changing the expand/collapse icons for the sap.m.Panel

Now, it's clear that we cannot simply mask the existing classes in the same way we did in the sap.ui.tree.TreeTable case. The reason is that in this case the icon is driven directly by an attribute value, not by change of style class. Since the icon is so clearly driven by the value of the attribute, your initial hunch might be to somehow change the value that is written out to the HTML. But this would involve rewriting or overriding the sap.m.Panel or its renderer, and we're not quite prepared to do that just to change the icon. But, there is a way. What we can do is write some rules that match the <span> depending on the value of the data-sap-ui-icon-content attribute. And if we can match a CSS selector based on the attribute value, we can simply write out a content property with the desired character instead. This works as long as we know what values the attribute will have, which is of course the case here, as there will only be 2 different values, corresponding to the collapsed or expanded state of the panel. This is what it looks like in ui5-customization-m.Panel.css:
/*
  sap.m.Panel better expanded button. 

  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57839, or 0xE1EF, which corresponds to UI5's "slim-arrow-down" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=slim-arrow-down)
  
*/
div.sapMPanel.sapMPanelExpandable > div > span[data-sap-ui-icon-content=].sapUiIcon::before {
  content: '\e1d9';
}

/*
  sap.m.Panel better collapse button
  
  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57837, or 0xE1ED, which corresponds to UI5's "slim-arrow-right" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=slim-arrow-down)
*/
div.sapMPanel.sapMPanelExpandable > div > span[data-sap-ui-icon-content=].sapUiIcon::before {
  content: '\e1da';
}
Note the span[data-sap-ui-icon-content=].sapUiIcon::before is the essential bit that allows us to react to a specific icon value. The selector part before is there to ensure the rule will only apply to the expand/collapse button of a Panel, and not to some random other control's icon. And, here's what it looks like in the sample app: sap.m.Panel with modified expand/collapse icons.

How sap.m.Tree renders the collapse/expand icons

The sap.m.Tree uses exactly the same mechanism to render the icons as the sap.m.Panel does - the character that corresponds to the appropriate icon glyph is written to a data-sap-ui-icon-content, and the value of the attribute is rendered against the icon font's font face. The only difference with the Panel is that the sap.m.Tree uses the navigation-right-arrow and navigation-down-arrow icons, just like the sap.ui.table.TreeTable did. Apart from that, we also need to ensure the first bit of the selectors are specific to the sap.m.Tree, which is similar to what we did for the sap.m.Panel. This is what the CSS looks like in ui5-customization-m.Tree.css:
/**
  sap.m.TreeItem : better icons for collapsed 

  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57446, or 0xE066, which corresponds to UI5's "navigation-right-arrow" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=navigation-right-arrow)

*/

li.sapMTreeItemBase > span[data-sap-ui-icon-content=].sapMTreeItemBaseExpander.sapUiIcon::before {
  content: '\e1da';
}


/**
  sap.m.TreeItem : better icons for expanded

  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57826, or 0xE1E2, which corresponds to UI5's "navigation-down-arrow" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=navigation-down-arrow)
*/

li.sapMTreeItemBase > span[data-sap-ui-icon-content=].sapMTreeItemBaseExpander.sapUiIcon::before {
  content: '\e1d9';
}
And this is what the Tree looks like in the sample app: A sap.m.Tree with modified expand/collapse icons.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

UI5 Tips: Buffering Events to avoid a request-storm

Standard UI5 event handling will usually go a long way. Yet sometimes, certain user actions can cause ui5 objects to generate a lot of similar events within a small period of time, and it is often not useful to handle each and all of them: only the last event needs handling. A very common scenario is doing a search in response to the liveChange event: if you'd attach a handler to handle the liveChange event, and do the backend query from there, then a backend request would be sent for each keystroke while the user is typing in the search field. This causes a storm of requests that the backend must somehow handle. But most of these requests will be for naught, as the user is only interested in the result of the query that matches the last complete search term they typed. So, rather than firing a query to the backend for each and every keystroke, it makes more sense to buffer these events, and react to only the last one. The bufferedEventHandler utility helps you to do just that in a generic and reusable way. This ui5tip describes the bufferedEventHandler utility. It is available on github under terms of the Apache 2.0 License. There's also a sample application so you can try it out yourself.

The BufferedEventHandler sample app

The bufferedEventHandler sample application illustrates the scenario from the introduction. It consists of a single page showing mockup company data in a sap.ui.table.Table. A screenshot is shown below: Screenshot of the BufferedEventHandler sample app. At the top left of the grid, there's a sap.m.SearchField labeled "Search in Name". The user can type some search term into the searchfield, and the grid will automatically refresh and show only the rows for which the CompanyName has a case-insensitive match with the entered search term. While the search happens automatically, it does not happen immediately as the search term changes at every keystroke. Rather, about 1 second after the user stops typing, the data grid is filtered. At the top right of the grid, there's a sap.m.ProgressIndicator labeled "Event buffer Timeout". The progress indicator reflects how much time has passed since the last keystroke. When the progress indicator reaches a 100%, the filter action is executed.

The bufferedEventHandler Utility

To buffer events we provide a bufferedEventHandler utility object with just one bufferEvents function. You can find this in the bufferedEventHandler file in the utils directory. To use it, we need to import it into the source file where we want to use it. This will usually be in a ui5 controller and in the sample app we do this in MainPage.controller.js:
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/table/Column",
  "sap/m/Text",
  "sap/ui/model/Filter",
  "sap/ui/model/FilterOperator",
  "sap/ui/model/FilterType",
  "ui5tips/utils/bufferedEventHandler"
], 
function(
  Controller,
  Column,
  Text,
  Filter,
  FilterOperator,
  FilterType,
  bufferedEventHandler
){
 "use strict";  
  var controller = Controller.extend("ui5tips.components.mainpage.MainPage", {
    ...
  });
  return controller;
}
We can now refer to the bufferedEventHandler utility through the local variable that is also called bufferedEventHandler. The controller uses the bufferedEventHandler utility in the initSearchField() method. This called from the controller's standard onInit() lifecycle method, which is called just once for the Controller instance:
  ...
  onInit: function() {
    this.initSearchField();
  },
  initSearchField: function(){
    var searchField = this.byId('searchField');
    bufferedEventHandler.bufferEvents(
      // event provider
      searchField,
      // timeInterval
      1000, 
      // eventId
      'liveChange', 
      // data
      null, 
      // handler
      this.doSearch, 
      // listener
      this,
      // progressHandler
      this.searchFieldProgress,
      // progressUpdateInterval
      50
    );
  },
  ...

The bufferEvents() Method

The meat of the initSearchField() method is the call to the bufferEvents method of the bufferedEventHandler utility. This method has the following arguments:
  • eventProvider: the 1st argument should be the object that emits the events - in our example this is the sap.m.SearchField. This object should be a subclass of sap.ui.base.EventProvider. (bufferEvents will throw an error if it's not!)
  • timeInterval: the 2nd argument is the timeout, in milliseconds. This is the amount of time that should pass between the occurrence of the last event and the call to the actual handler of the event. If a new event occurs during the wait period, the timeout is reset, and a new waiting period is started. In the example, we use a timeInterval of 1000 - that is, we will wait 1000 milliseconds (1 second) before handling the last event.
Choosing the timeInterval is a balancing act. In the case of the example, where the events are generated in response to user actions, the timeInterval should not be too short, as the user should be given enough time to type a meaningful searchterm before the actual query kicks in. But if the timeInterval is too long, the application may appear unresponsive to the user. If the application appears unresponsive, the user may try to retype their search term, which will only postpone the reaction even more. (There's more about this in the section about the ProgressIndicator). The next 4 arguments of bufferEvents correspond to sap.ui.base.EventProvider's attachEvent() method:
  • eventId: a string that identifies the event to listen to. In our example this is 'liveChange'. Some ui5 objects, (for example, sap.ui.base.ManagedObjects, which includes all sap.ui.core.Controls) expose the events they expose through their metadata. In these cases, bufferEvents will verify whether the passed eventId is in fact exposed by the object, and it will throw an error in case it doesn't. EventProviders that do not expose their events through metadata can still be used with the bufferedEventHandler, but you'll just need to make sure yourself the value for eventId is valid, as bufferEvent has no way of checking it.
  • data: an optional argument to pass any "extra" data that the event handler might need. In the example, we pass null as we have no need for any additional data.
  • handler: this should be the callback-function that will be called upon to actually handle the event. The callback function will receive an instance of an sap.ui.base.Event as single argument, which typically provides access to all relevant information pertaining to the event. In the example, we pass this.doSearch, which is a method of the controller that will perform the actual filtering of the data grid.
  • listener: this is an optional argument which you can use to specify the scope in which the handler will be called. Typically the handler will not be completely standalone, but it will refer to a this object, one way or another. If the handler function is not already bound (for example, by using the function's bind() method), then you should pass whatever object should act as this for the handler function via the listener argument. In the example, we simply use this which refers to the controller instance itself. This makes sense as the handler function is also a method of the controller. (Remember: we passed this.doSearch as handler.)
In the call to bufferEvents, these arguments will be used to create an actual handler for the event, and also automatically attach it to the eventProvider for the specified eventId. But rather then calling the passed handler, it will start a javascript timeout for a duration of the passed timeInterval. If the timeout was already initiated, it is cleared, thus canceling the previous event, and initiating a new waiting period.

Monitoring wait progress

The final 2 arguments to bufferEvents are optional, and may be used by the application to monitor the waiting period between the occurrence of the last event and the time when the handler will actually be called:
  • progressHandler: when passed, this should be a callback function which is to be called at the start and during the waiting period. If the progressHandler callback is called, it will be called using the listener as scope. The callback will be passed a floating point number between 0 and 1, indicating the fraction of the time that has passed between the last event and now. If a progressHandler is specified, it is always called at least once and passed 0 whenever a new waiting period is initiated. In this example we passed this.searchFieldProgress, which is a method of the controller that updates the sap.m.ProgressIndicator that sits in the right top of the data grid.
  • progressUpdateInterval: this should be in integer, indicating the number of milliseconds between the calls to the progressHandler. In our example it is 50, which means we will get 1000 / 50 = 20 updates during the waiting period, which ensures a smooth and regular update of the ProgressIndicator control.

The ProgressIndicator

The sample application provides a sap.m.ProgressIndicator to indicate when the entered search term will be used to filter the data. A progress indicator may not be necessary in case the timeInterval is so short that it will appear to the user as if the event is handled immediately. But when the timeInterval exceeds 200 or 250 milliseconds, most users will start to experience a noticeable lag. Now, there is this strange psychological phenomenon happening here - as the user is still typing their search term, they will be happy that the backend query is not already fired. It would make them feel rushed if the grid was constantly being updated while they were typing. But once the user is done typing their search term, they want to have the result as quickly as possible. Obviously, the software cannot read the user's mind (yet!), so once the user stops typing, the application needs to let the user know they acknowledged their action, and that it is 'working on it'. Hence the need for a progress indicator: by having a visual indicator that "something's happening", the user will be assured the application has acknowledged their input, and this will make the wait period before actually handling the event more acceptable. If the wait is sufficiently short, a simple busyIndicator might do the trick, but since the progressHandler gets passed an exacte estimate of how much longer the user will need to wait, our progress indicator can communicate this to the user. This will make the application's behavior more predictable and hopefully more satisfying to use. Of course, it is not absolutely necessary to use the sap.m.ProgressIndicator to give this kind of feedback to the user. It's just that for this sample, this was the easiest, most straighforward illustration of this principle. You can use the `progressHandler` callback to do anything you like to fit your need.

Detaching

The bufferEvents method will create and attach a handler to the eventProvider. bufferEvents will also return that generated handler so you can detach it explicitly from the eventProvider if you need to. As a convenience, the returned handler provides its own detach() method for this purpose:
var bufferedEventHandlerInstance = bufferedEventHandler.bufferEvents(...);
...
bufferedEventHandlerInstance.detach();
(Note that in a typical scenario, the eventHandler and the eventProvider will almost certainly be in the same scope and lifecycle, so there is rarely a need to explicitly do this.)

Other Use Cases

The liveSearch scenario may not always be a convincing use case. For example, if the query is done against a client model rather than a remote backend system then it might not actually be a problem to re-issue the query for every keystroke. But there are some other scenarios that benefit from event buffering. We will encounter one such case in the tip about Persisting UI State.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

UI5 Tips: Manipulating the sap.m.TabContainer close buttons with custom CSS

Here's a ui5tip to show how you can change the look and feel of the sap.m.TabContainer with a minimal amount of custom CSS. If you want to try this for yourself, be sure to check out the sample application from github.

The sap.m.TabContainer

The sap.m.TabContainer provides a simple, no-nonsense widget to build a tabbed user interface (check out the samples). Tabs can be added via the items aggregation, which should contain a collection of sap.m.TabContainerItem's. While this control generally suits my needs, it has one feature I find problematic: each tabs always has a close button, which appears as a little 'X' icon in the right side of the tab. If the user clicks it, it will actually 'close' the tab, that is: the respective sap.m.TabContainerItem will be removed from the TabContainer. See the screenshot below to see what the default looks like (close buttons highligthed in red):default sap.m.TabContainer with close buttons on each tab.

Suppressing the close action

The openui5 samples show how you can suppress that behavior: you can write an event handler for the itemClose event, and then call the preventDefault() method on the event: (In the view xml:)
<m:TabContainer itemClose="onTabContainerItemClose">
  <m:items>
    <m:TabContainerItem>
       ...
    </m:TabContainerItem>
    <m:TabContainerItem>
       ...
    </m:TabContainerItem>
  </m:items>
</m:TabContainer>
(In the controller javascript:)
  onTabContainerItemClose: function(event){
    event.preventDefault();
  }
Obviously, it would be strange if we'd always prevent the tab from being closed: Suppressing the default action of closing the tab only makes sense in a context where the user is supposed to be able to close the tab at all, and in such a case this could be used to pop up a dialog to let the user choose if they really meant to close the tab or want to keep it open. But the use case I frequently encounter is that the tab should not be closeable in the first place. While suppressing the close action would ensure the tab is never closed, it would confuse and anger the user, as the close button itself would still be there, inviting users to perform an action that can never be fulfilled.

Using the other Tab widget

One might suggest to use the sap.m.IconTabBar widget instead of the sap.m.TabContainer. The sap.m.IconTabBar takes sap.m.IconTabFilter's in its items collection, and these do not have a close button. Now, in some cases the sap.m.IconTabBar/sap.m.IconTabFilter may suit your needs and then you're fine. However I find that it has a number of other drawbacks (which I won't get into here). Besides, the sap.m.IconTabBar introduces a similar problem, but the other way around: whereas we cannot get rid of the close button in the sap.m.TabContainer, we cannot ever have a close button in the sap.m.IconTabBar. What we really want, is a property or something like that, which will let us control whether the tab will have a close button or not.

CSS to the rescue

In the previous section we argued that we'd really like to be able to control for each individual tab whether they have a close action at all, for example, by setting a property. To add a property one would normally have to extend a ui5 control, and attach some code so that the property setting can somehow influence the behavior of the control - in this case, control whether or not the close button will be displayed. While this is probably possible (I haven't tried it for this case), it does seem like an extraordinary measure for such a humble request. I found that a similar effect can be achieved by applying some custom CSS in combination with standard ui5 features. That's what this entire sample is about. With this tip you can:
  • hide all close buttons for an entire sap.m.TabContainer
  • hide the close button on an individual sap.m.TabContainerItem
  • show the close button on an individual sap.m.TabContainerItem in case the close buttons are hidden by default on the sap.m.TabContainer
All this functionality requires the inclusion of some css. In the sample this is all isolated in a single ui5-customization.css file, which is included into the application by declaring it the manifest.json.

Hiding all close buttons

To hide all the close buttons for all sap.m.TabContainerItem in the items collection of a particular sap.m.TabContainer, simply add the noCloseButtons style class:
<m:TabContainer 
  class="noCloseButtons" 
>
  <m:items>
    <!-- note: close button will be hidden by default for each m:TabContainerItem -->
    ...
  </m:items>
</m:TabContainer>
This works because the class property in the ui5 xml view is rendered to the html dom directly. So whatever html elements that ui5 creates to implement the TabControl widget will then be selectable with a class selector in css, and this is how we can relatively simply influence the look of our TabContainer through css. In our ui5-customization.css file, this is how we use noCloseButtons class to hide the buttons:
div.sapMTabContainer.noCloseButtons > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem > .sapMTSItemCloseBtnCnt {
  visibility: hidden;
}
(Note the initial selector, div.sapMTabContainer.noCloseButtons and the chain of > child selectors which target the actual bit of html that is used to render the close button, which is simply hidden by setting the css visibility property to hidden.) In the sample application, you can see this behavior in action in the App.view.xml. This contains the code for the outermost tabcontainer. A screenshot is shown below and as you can see both tabs ("Hide individually" and "Show individually") do not have a close button: Hiding the close buttons on a sap.m.TabContainer by applying the noCloseButtons css class.

Hiding an individual close button

If we can add a custom css class to sap.m.TabContainer to hide all close buttons, then surely it should be possible to follow the same approach for an individual sap.m.TabContainerItem, right? Yes, it should, but sadly, we cannot. (The reason is that sap.m.TabContainer is a subclass of sap.m.Control, which provides a addStyleClass() method, whereas sap.m.TabContainerItem is a subclass of sap.ui.core.Element, which does not have such a method) Now, let's take a step back and think about how we used the css style class on the sap.m.TabContainer to hide all the close buttons. By setting the custom css style class on the sap.m.TabContainer, the html dom was changed to include the custom class, and we could then use that in a css selector. So even if we cannot apply a css style class to a sap.m.TabContainerItem, might there be another way that would allow us to influence how ui5 writes the html dom so we may write a css selector in our custom css? It turns out that such a feature exists in the shape of a feature called ui5 custom data. The custom data aggregation is provided by sap.ui.core.Element and thus available to its subclasses, including sap.m.TabContainerItem. A custom data item is an arbitrary key/value pair, and by setting its writeToDom property, ui5 will render it to the html dom as a html data attribute. To see what it looks like in our sample, take a look at TabContainerItemWithHiddenCloseButton.fragent.xml, which uses it to hide the close button in the second sap.m.TabContainerItem, in an otherwise normal sap.m.TabContainer:
<m:TabContainerItem
  id="item2"
  name="No Close Button"
>
  <m:customData>
    <core:CustomData writeToDom="true" key="noCloseButton" value="true"/>
  </m:customData>
  ...
</m:TabContainerItem>
Because the CusomtData's writeToDom property is set to true, ui5 will generate a html data attribute to the html dom that looks something like this:
data-noclosebutton='true'
And in our ui5-customization.css file, the following rule is intended to pick that up and hide the close button:
div.sapMTabContainer > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem[data-noclosebutton='true'] > .sapMTSItemCloseBtnCnt {
  visibility: hidden;
}
As you can see it is very similar to the rule we used to hide all close buttons on any sap.m.TabContainer having the noCloseButtons class, except now the class is missing and instead we use a css predicate selector based on the data attribute:
.sapMTabStripItem[data-noclosebutton='true']
The screenshot below shows what it looks like in the app. Note that the tab named "Default" has the close button as usual, but the one named "No Close Button - the one with the custom data attribute - does not show a close button: Individual TabContainerItem with a hidden close button.

Showing an individual close button

The final hack in this sample combines the style class and the custom data attribute. CSS allows us to write a selector that takes both the presence of the css style class as well as the presence of a html data attribute into account. We can put this to good use if we want to have a sap.m.TabContainer that hides all close buttons by default, but undo the hiding of the close button of specific sap.m.TabContainerItem's, based on the value of a data attribute (which is in turn controlled by the ui5 Custom Data feature). You an see this in action in the TabContainerItemWithHiddenCloseButtons.fragment.xml file of the example:
<m:TabContainer 
  class="noCloseButtons"
>
  <m:items>

    <m:TabContainerItem name="Default">
     ...
    </m:TabContainerItem>

    <m:TabContainerItem name="Show Close Button">
      <m:customData>
        <core:CustomData key="noCloseButton" value="false" writeToDom="true" />
      </m:customData>
      ..
    </m:TabContainerItem>

  </m:items>
</m:TabContainer>
Again the second sap.m.TabContainerItem has a CustomData item with the key noCloseButton, but now the value is true so as to override the effect of the noCloseButtons style class applied to the sap.m.TabContainer. In our our ui5-customization.css file, the following rule is intended to pick that up and show the close button:
div.sapMTabContainer > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem[data-noclosebutton='false'] > .sapMTSItemCloseBtnCnt {
  visibility: visible;
}
The screenshot below shows what it looks like when you run the sample application: Show the close button in an individual TabContainerItem, overriding the effect of the noCloseButtons style class of the sap.m.TabContainer.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

UI5 Tips: Adding a Splash-Screen / Loading indicator

UI5 apps always take a little time to load. If you don't take any precautions, the user will be looking at a blank screen while the app is loading. It is fairly simple to add a splash screen and loading indicator to improve the user experience. This will give your app a more professional appearance, and it will cost minimal effort. This post shows you exactly how to do this. You might also be interested in checking out the sample app from github so you can run it yourself.

Running the app

The loadingscreen sample app is in the loadingscreen subdirectory of this repository. Simply expose the folder and all its contents with a webserver, and navigate to its index.html

How does it work?

In various tutorials and the ui5 walkthrough, the <body> is usually left empty. This is for a good reason - any content that is in the body would simply show up on the page, as is shown in the walkthrough's "Hello world!" example. But there is an exception: if an element has an id attribute with the value busyIndicator, then this content will be hidden after the ui5 bootstrap code is finished and the app content is placed inside the body. In the loadingscreen sample, the index.html page has a static <div> element with an id attribute with the value busyIndicator as static content in the <body> element. Indside that <div> we can place anything we like. In the example, the div contains a header and a footer with static text to indicate that the application is loading. Between the header and the footer is an image of the ui5 logo, and a css animation, which superficially resembles the ui5 busy indicator animation:
<body class="sapUiBody" id="content">

  <!-- Loading splash screen -->
  <div id="busyIndicator" style="text-align: center; font-family: Sans, Arial">
    <!-- static header text -->
    <h3>My UI5 App is loading</h3>
    
    <!-- static image of the ui5logo  -->
    <img src="images/openui5-logo.png" class="logo"/>

    <!-- 
         loader animation 
         CC0 licensed code used with permission from 
         https://loading.io/css/
    -->
    <div class="lds-ellipsis">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
    <!-- end of loader animation -->

    <!-- static footer text  -->
    <center><h5>This may take a few moments...</h5></center>
  </div>
  
</body>
The css animation requires some css, and this is included simply as a static css resource by including an appropriate <link> element in the <head> of the page:
<head>

  <!-- css required for the loading screen css anumation -->
  <link id="animation" rel="stylesheet" type="text/css" href="css/progress-animation.css"/>
   
</head>
In this case, we pulled the css animation from the excellent site https://loading.io/css/, which provides many different free css animations. Note that any css required by the loading screen must really be included statically via the <link> or <style> element. The standard ui5 mechanism to include css by declaring it in the manifest.json is no good as it will be loaded as part of the ui5 bootstrap, and the whole idea of the loading screen is to show something before the ui5 bootstrap even starts.

What does it look like

This is what it looks like when the app is loading: Screenshot of the loading screen The text, logo image, and the loader animation are all static content and will show during UI5 bootstrap.

Next Steps

Obviously, this loading screen is only an example to show how you can make it work. The entire design of the loading screen is up to you. Just remember to keep it light and quick: the whole reason to include a loading screen in the first place was to give the user something to look at while ui5 is bootstrapping. If the loading screen itself requires a lot of resources then it defeats its purpose. For this reason, you might consider including any css directly using the <style> element, rather than relying on a network request to load external css with the <link> element.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

DuckDb Performance: min, max, and median vs quantile

I was playing with some classical statistics in DuckDB and I ran into something I'd like to share. It's about the measures minimum,...