Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
jthuijls
Participant
7,630
G'day.

A while ago I wrote a small but convenient extension for the good old JSON model, because I was looking for an easy way to persist data in between sessions in a UI5 application. My client was keen on functionality to save filter settings in a search screen with quite a lot of options and they'd love it if their last filters would just reload if the app opened.

See it live in this codepen


Here's my train of thought:

  • I need to persist data between sessions, my app will be closed completely.

  • I know localStorage can be used to save data in the browser

  • I know I can store things in the JSON model

  • I know that the bindings use getProperty and setProperty internally


Hence my conclusion to simply extend the JSONModel and redefine setProperty and getProperty to first check the localStorage, the model + binding functionality itself will do all of the heavy lifting for me. Nifty? Yes. Easy? Also yes.

This is the bare minimum.

LocalStorage basics


localStorage can be used for key/value pair storage in the browser itself. Most modern browsers can store quite a lot of data. According to this Wikipedia article, the minimum is 5MB per host, but users have reported far higher values across different browsers.

Since it's key/value pairs, we should add a check to see what we're storing. If it's a JSON object, it should be stringified going in, and parsed coming out for instance.

LocalStorage is not secure, so heads up


By popular request, an added word of warning here about the accessibility of the data stored in LocalStorage. Most browser based storage is not secure, so be careful with sensitive data. As pointed out in the comments, people with access to your laptop, and any code running on the same domain - this includes third party scripts or scripts loaded via CDN - will have access to your LocalStorage. So, only store data that is not meant to be confidential.

LocalstorageJSONModel constructor


constructor: function(data, aFieldsToRetain) {
this._fieldToRetain = aFieldsToRetain || [];
this._storage = Storage;

if (!(this._fieldToRetain instanceof Array))
throw new Error(
"Fields to retain for local storage needs to be an array"
);

//good old constructor call to set the data
JSONModel.prototype.constructor.call(this, data);
},

We need a way to tell the JSONModel which fields to store, so addition to the data object you see the fieldsToRetain array. This will contain a list of properties that you'd like to be stored. After that, the normal constructor is called.

Storage is the window.localStorage object, I figure I'd abstract it out in case I want to switch to cookies, sessionStorage, or maybe something from Cordova / PhoneGap / Mobile services. You never know!

setProperty


setProperty: function(param, value) {
if (this._fieldToRetain.indexOf(param) >= 0) {
const toStore = typeof value === 'object' ? JSON.stringify(value) : value;
this._storage.setItem(this._getLocalStorageParamName(param), toStore);
}

JSONModel.prototype.setProperty.call(this, param, value);
},

_getLocalStorageParamName: function(param) {
return `myapp${param}`;
}

That's all. _getLocalStorageParamName is a convenience method to generate a more unique identifier. I use the logged in userID and the application name from the manifest myself, don't want to run into trouble between different apps (launchpad, I'm looking at you).

All you see here is a call to see if the current parameter is in the list of parameters and if it is, pass it to localStorage.setItem.

If the value is an object, it should be stringified, otherwise it won't work, because javascript will just call toString which means you'll end up with [Object object] and 1,2,3 instead of your object or array.

getProperty


getProperty: function(param) {
var prop = this._storage.getItem(this._getLocalStorageParamName(param)) ||
JSONModel.prototype.getProperty.call(this, param)
var value;

//JSON.parse can actually crash, so the try/catch block is justified
try {
value = JSON.parse(prop);
} catch (e) {
value = prop;
}

return value;
},

First, it tries to fetch the parameter from localStorage and if it comes up empty, it calls the standard function and fetches from the JSONModel.

Then, parse the results. JSON.parse crashes when fed rubbish, so if that call fails it was not stringified and we'll return it the way it was.

Usage


const fieldsToRetain = ["/name"];

let model = new LocalstorageJSONModel({
name: 'Jorg',
country: 'Australia'
}, fieldsToRetain);

This will remember the name forever and reload it when requested. If someone enters Jane in the name field, the next time the app is opened the name Jane appears regardless of how it's initialised.

Here's that demo again in case you made it down here and now you want to see it run. Be sure to type in your name and reload the page.

See it live in this codepen


In conclusion


That's it. Offline capabilities and semi persistent storage with minimal effort. The drawback here is that the localStorage is - to a degree - out of the developer's control and can be cleared quite easily by the user so it should not be used to store anything serious or confidential.

Feel free to fancy it up to suit your needs obviously. My own has convenience methods to store and regenerate whole nested sap.ui.model.Filter objects for instance since I seem to be using that a lot. Added bonus is that, without the fieldsToRetain object it's just a JSONModel, so I turned it into a library and use it for everything. If you want to get rid of it again, change the import in the definition and it's like nothing ever happened.

What do you reckon?

Full model


LocalstorageJSONModel.js
sap.ui.define(["sap/ui/model/json/JSONModel", "./Storage"], function(
JSONModel,
Storage
) {
"use strict";

return JSONModel.extend("demo.LocalstorageJSONModel", {
_fieldToRetain: false,

/*
constructor still takes the data object, but also an array of fields
to remember.
*/
constructor: function(data, aFieldsToRetain) {
this._fieldToRetain = aFieldsToRetain || [];
this._storage = Storage;

if (!(this._fieldToRetain instanceof Array))
throw new Error(
"Fields to retain for local storage needs to be an array"
);

//good old constructor call to set the data
JSONModel.prototype.constructor.call(this, data);
},

//redefine the getProperty method. Before the JSON model loads it's own content, I want
//to see what's inside the localstorage, and I'd like to parse it if it's JSON
getProperty: function(param) {
var prop = this._storage.getItem(this._getLocalStorageParamName(param)) ||
JSONModel.prototype.getProperty.call(this, param)
var value;

//JSON.parse can actually crash, so the try/catch block is justified
try {
value = JSON.parse(prop);
} catch (e) {
value = prop;
}

return value;
},

//convenience method. sometimes you just want to clear all variables.
clearStorage: function() {
this._storage.clear();
},

//redefine setProperty to first check if the field is in the fields to retain.
//in that case, send it to localstorage as well as do the parent method.
setProperty: function(param, value) {
if (this._fieldToRetain.indexOf(param) >= 0) {
const toStore = typeof value === 'object' ? JSON.stringify(value) : value;
this._storage.setItem(this._getLocalStorageParamName(param), toStore);
}

JSONModel.prototype.setProperty.call(this, param, value);
},

//you should create unique identifiers. in productive apps this needs more consideration. i use
//app id from the manifest and user ID.
_getLocalStorageParamName: function(param) {
return `myapp${param}`;
}
});
});

Storage.js
sap.ui.define([], function() {
return window.localStorage;
});
9 Comments
Labels in this area