Skip to Content

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;
});
To report this post you need to login first.

8 Comments

You must be Logged on to comment or reply to a post.

    1. Jorg Thuijls Post author

      Thanks. Checking now if this can be done with the oData model as well. Would be nice to easily store application master data and other non essential info

      (0) 
    1. Jorg Thuijls Post author

      Hi Pierre,

      Thanks for the alternatives. The backend storage is nice on the smart filter bar With this model though, I don’t need to change anything in my applications to store everything (but it only persist in browser).

      (0) 
  1. Chris Paine

    This is very cool. Just to flag to those who might not think about it, that local storage is not secure, so don’t store anything that that a hacker with access to your machine shouldn’t be able to see. (To be fair if a hacker has access to your machine, you’re screwed anyway 😁)

    Nice one Jorg

    (1) 
  2. Mike Doyle

    Nice work Jorg Thuijls and thanks for sharing.  I use JSON models a lot and I have also used local storage to store a few filters and the like.  In my experience, small steps like this can make a big difference to UX.

    As Chris Paine  points out we probably should emphasise to people that they shouldn’t ever put anything sensitive in there, as it isn’t meant to be secure.

    Aside from the point Chris made about people having access to the machine (or using a shared machine) any data in local storage would be vulnerable to a cross-site scripting (XSS) attack.  It seems pretty unlikely for a UI5 app but it’s becomes more likely as we add outside code like external libraries, Google Analytics etc.  Also, there is a move to download the UI5 libraries from a Content Delivery Network (CDN).

    I would probably name the JSON model something like “public” so it’s clear sensitive data shouldn’t go in there.

     

     

    (2) 

Leave a Reply