Skip to Content
Technical Articles

Better UX for Fiori Apps with HTML5 Web Storage

Fiori apps are simple and delightful, among others 😉 And the more we move towards using Fiori more often, to fulfill these points gets more and more important – and at times more challenging.
Certain apps are used very often, maybe several times per day and maybe certain data is put in again and again: and here we can help the user with a very simple UI5 controller, that we can add to our apps; the html5 web storage controller.

Web Storage

With the html5 web storage, web applications can store data locally within the user’s browser. Before HTML5, application data had to be stored in cookies, included in every server request. Web storage is more secure, and large amounts of data can be stored locally, without affecting website performance. Check here for more information.

You may know or read in above link, that the html5 web storage has two objects:

  • window.localStorage: stores the data with no expiration date
  • window.sessionStorage: stores the data for only one session

Depending on what suits your app best, use either object. Both have their purpose and can be used in a smart way, to help out our users.

Regarding Security!

Please note that this method should only be used for non-sensitive data. The data values are stored as you save them into the web storage. Only the same domain can read properties that were saved through that domain (if domain1.com saves “somedata” then domain2.com cannot read “somedata”). But if anyone gets access to your computer, the data could be retrieved from your browser. Also using this method, to save something locally in a user’s browser, and then use that data to send a server request is very risky; because then you open up a way for the hacker to even reach the server, after hacking your web storage.

A way to do it could be to have an encryption key on the server, that you would first need to retrieve through a regular sever request and then you use the key to encrypt data, before you save and also decrypt data, after you retrieved them. But even before looking into this, I would double check with your security officer, if you are unsure about certain data you want to store.

So below I will be talking without encryption for non-sensitive data.

Create a Controller

To use and reuse the htlm5 web storage, we can create a controller, which we then initialize in our app’s component or main controller (see below for an example, how to use it).

Depending on your needs, you could create a general controller Storage.js or one per web storage object, for example LocalStorage.js and SessionStorage.js. This is fully up to preference.
For simpleness sake, I will below use Storage.js.

We want to use our web storage, as a simple object so we can make use of sap.ui.base.Object:

sap.ui.define(["sap/ui/base/Object"], function (Object) {
	"use strict";

	return Object.extend("[NAMESPACE].[APP].controller.Storage", {

		_oStorage: false

	});
});

What the above simply does, is returning a sap.ui.base.Object. This will be our web storage, which we extend below, to hold functions for the storing and reading of data.
For now we just have one private variable _oStorage which is set to boolean false. We will use this, to make sure that the user’s browser supports the html5 web storage. If not, no error will occur and the web storage is simply not used, since it can’t be used.

Constructor (with compatibility check)

First of, as just mentioned, we need to init our controller and make sure, the user’s browser is even able to use html5 web storage. Here an example for window.localStorage:

		_oStorage: false,
		
		/**
		 * Create storage object or keep it as false
		 * @public
		 */
		constructor : function () {
			if(!window.localStorage) {
				return;
			}
			this._oStorage = window.localStorage;
		},

You may do the exact same for window.sessionStorage:

if(!window.sessionStorage) { return; }
this._oStorage = window.sessionStorage;

 

Or you could pass a parameter into the constructor, deciding which type of storage to create dynamically:

		/**
		 * Create storage object or keep it as false
		 * @public
		 * @param {boolean} bSession
		 */
		constructor : function (bSession) {
			if(bSession === true) {
				if(!window.sessionStorage) {
					return;
				}
				this._oStorage = window.sessionStorage;
			} else {
				if(!window.localStorage) {
					return;
				}
				this._oStorage = window.localStorage;
			}
		},

Going forward, I will only show one example – but I believe above shows that you can fully decide what is best for your app.

Simple Read and Write Functions

So, now that we have our web storage object, let’s use it. The HTML5 web storage is so simple, we just need to give a name and value and that’s it; we saved data into the user’s browser. However that name of course should be unique (per domain). Imagine this; you use this same controller in several apps in your Fiori Launchpad (same domain) and have a parameter called “mydata” in both apps but with different values. But you won’t actually save two different data -one per app-, instead the apps would keep on overwriting “mydata”. Maybe for some data fields, that is your main purpose, let’s say for example a plant code of the user. But in my example below, I show you how to avoid non-unique web storage data; with a helper function:

		/**
		 * Return storage parameter name, that this app uses
		 * @private
		 * @param {string} sParam parameter name
		 * @returns {string} full storage parameter name for this app only!
		 */
		_getStorageParamName : function (sParam) {
			return "my_app{" + sParam + "}";
		}

So here we have a super simple function, that uses the name we want for the parameter to save and adds an “app-name prefix”. Of course, “my_app” from above should be replaced with your actual app name or a short version of it.
Now if you send as parameter name “mydata”, for “app1”, the browser would store a data item called “app1{mydata}” and for “app2” it would be “app2{mydata}” instead.

=> This however means, you are required to change this controller per app, that you use it in. Otherwise you will have “my_app” everywhere an lose your uniqueness again.

Once again this is a help function, that can be used or not but I do recommend it and you will find it in codes posted below.

Now let’s add the write/save function:

		/**
		 * Save one parameter and value to storage
		 * @public
		 * @param {string} sParam - parameter name
		 * @param {mixed} value - parameter value
		 */
		setStorageParam : function (sParam, value) {
			if(this._oStorage === false) {
				return;
			}
			var store = 
				typeof value === "object" ? 
				JSON.stringify(value) : value;
			this._oStorage.setItem(
				this._getStorageParamName(sParam), 
				store
			);
		},

Looks bigger than it is, so here is what it does:

  1. First of we make sure that we could get the web storage object. If not we exist and do nothing (no error)
  2. Next, we set a new local variable called store with the value we want to save. Variable value was passed into this function, but since web storage is so powerful, we are in fact able to save js object variables. So we check with typeof if the passed value is an object. If yes, we use JSON.stringify to do exactly as it says; stringify the object into a JSON string. If we didn’t get an object, we have a string or number etc., so then we simply set that value.
  3. Lastly, now that we have a correct value to save in the store variable, we use the “save” function of our web storage object called setItem. Set item expects a name and a value, and as explained above, here I’m using the helper function to set a unique name

Awesome, we can now save data into the user’s browser. But of course we need to be able to read it as well. So here’s the read function:

		/**
		 * Try to get one parameter and value from storage
		 * @public
		 * @param {string} sParam - parameter name
		 * @return {mixed} parameter value
		 */
		getStorageParam : function (sParam) {
			if(this._oStorage === false) {
				return false;
			}
			var prop = 
				this._oStorage.getItem(this._getStorageParamName(sParam)),
				value;
			
			// JSON.parse can crash, so the try/catch block is justified
			try {
				value = JSON.parse(prop);
			} catch (e) {
				value = prop;
			}
			
			return value; 
		},

And here is what above means:

  1. We make sure we have the web storage object and if not, do nothing (no error)
  2. We use the web storage object’s read function called getItem and we once again use the helper function, to get that unique parameter name, so we get the correct stored value back. This is put into variable prop and we prepare the value variable as well.
  3. Next, once again because we could have saved a JSON string object, we use JSON.parse, to get the proper value and catch, in case of errors and only then, set the value to return directly.
  4. Lastly we return the value

We are almost done. Now we need some cleanup functions too.

Cleanup Functions (localStorage)

With the above we can save data and read it without ever needing to send a server request. But at times we want to remove these locally saved data, especially if we used localStorage, which saves data without expiring, meaning even after the user logs out or closes the browser, the data will still be saved. We can cleanup in two ways: just one single data parameter or everything we saved with the web storage object, for that whole domain. And yes, this truly means everything from any app in our domain.

Let’s start with removing one parameter:

		/**
		 * Remove one parameter from storage
		 * @public
		 * @param {string} sParam parameter name
		 */
		clearStorageItem : function (sParam) {
			if(this._oStorage === false) {
				return;
			}
			this._oStorage.removeItem(this._getStorageParamName(sParam));
		},
  1. We check if we have the web storage object
  2. We simply delete the parameter with the web storage’s removeItem function (and once again, using the helper function for the unique parameter name)

Very simple. We can use this on a logout button, navigation or other events, when it could be needed.

To cleanup the whole web storage of the domain, should never be called by any app directly, in my opinion. I personally only added it, so that I could call this function while in the browser debugging tools, to use it in the console, to be able to start over. So I don’t in fact recommend ever calling this function in your app! Maybe you even want to remove this function from your controller, once you are done trying out and know you won’t need it. As said, you can clean/remove every single parameter you create, with the above function and cleaning everything, could also remove data saved by other apps of your domain! So big caution here please! But I will still share how it is done:

/**
 * Empty storage (all of it - use cautiosly!)
 * @public
 */
clearStorage : function () {
	if(this._oStorage === false) {
		return;
	}
	this._oStorage.clear();
}

Very simple: there is the clear function on the web storage object.

 

Use The Web Storage Controller

Now that we have our js file, we can add it to any app really. I personally put it into the same folder “controller”, as the app controller’s but of course, where you put it is up to you.

In my example, I show how to initialize the storage controller in your Component.js (but as mentioned before, this could be done in a base/main controller or on other places, depending how you mean to use it) and then how I can use it, in my view controllers.

1. Init the storage object

In my Component.js I import the storage controller object and then create it, in my component’s init function:

sap.ui.define([
	"sap/ui/core/UIComponent",
	"./controller/Storage"
], function (UIComponent, Storage) {
	"use strict";

	return UIComponent.extend("[namespace].[APP].Component", {
		metadata: {
			manifest: "json"
		},

		/**
		 * The component is initialized by UI5 
		 * @public
		 * @override
		 */
		init : function () {
			// call the base component's init function
			UIComponent.prototype.init.apply(this, arguments);
			
			// enable routing
			this.getRouter().initialize();
			
			// create local storage object
			this._oStorage = new Storage();
		},

2. Getter function (to get the storage object)

Next I add a simple getter function, so that my app controllers can easily get and use the web storage object:

		/**
		 * Getter for own storage object (Storage.js)
		 * @public
		 * @returns {/controller/Storage} own storage object
		 */
		getStorage : function () {
			if(!this._oStorage) {
				return false;
			}
			return this._oStorage;
		},

On top of this, I personally use a BaseController.js with helper functions, for even easier access to my storage object and others:

/**
 * Getter for own storage object (Storage.js)
 * @public
 * @returns {/controller/Storage} own storage object
 */
getStorage : function () {
	return this.getOwnerComponent().getStorage();
},

3. Use web storage in a controller

In my app I have a search view for articles. But because users often search the same articles, I save any clicked article, into a “recents” object. So when my Search controller is loaded, I read this web storage saved data:

/**
 * Create "recents" model and try to read from storage
 * @public
 * @param {string} sStorParam - name of the "recents" storage parameter
 * @return {sap.ui.model.json.JSONModel} recents model
 */
createRecentsModel : function () {
	var oData;
	// try to get from storage
	oData = this.getStorage().getStorageParam("recents");
	if(!oData) {
		// if nothing is saved to storage (or storage init failed)
		// we create/init model data
		oData = {
			count : 0,
			articles: []
		};
	}
	// return "recents" model
	return new JSONModel(oData);
},

The data is a simple object with a count and an array of recently clicked articles. If the storage parameter “recents” has data, the whole object is returned. If not, we init the data model.

Next, I of course display these recents on my view in a list. If a ListItem is pressed, I add the article into the array and save the data to the web storage:

/**
 * Save a clicked article to recents
 * @private
 * @param {object} oData - recents data
 */
_saveToRecents : function (oData) {
	var oRecentModel = this.getModel(this._sRecent),
		aArt = oRecentModel.getProperty("/articles");
	if(!aArt) {
		return;
	}
	// check if max recents is reached and remove (from the end) 
	// until a new recent can be saved
	while(aArt.length >= this._maxRecents) {
		aArt.pop();
	}
	// add new recent to beginning of array
	aArt.unshift(oData);
	// update models & refresh
	oRecentModel.setProperty("/articles", aArt);
	oRecentModel.setProperty("/count", aArt.length);
	this.byId("listRecent").getBinding("items").refresh(true);
	// save to local storage
	this.getStorage().setStorageParam("recents", oRecentModel.getData());
},

First we get the model and add the clicked article to it. Then we update the model and List on the view (because in my example I will navigate away from this view). Lastly, on the very last line, with just a one-liner, we save the whole recents object to the user’s local browser. And next time he opens the search view, all recently clicked article will be there visible immediately, without any server request.

 

Final Words

I hope this helps. Please let me know any thoughts or if you have questions. I tried to keep this short but if you want to know more, let me know.

And I hope my example gave a good use-case, when web storage saving can be used to help the user to use and enjoy our delightful Fiori apps 🙂

2 Comments
You must be Logged on to comment or reply to a post.
    • Hi and thanks for this comment. Yes, this is a good point and very possible, but also I wouldn’t say it’s best in all cases. Sometimes simple data may not need encryption as they are not true sensitive data. I would be careful with storing sensitive data in general and really think first, if it’s ok, to store the data at all. I personally don’t use this to store passwords, for example.

      But thanks, I will edit in a line, talking about this!