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: 
former_member189945
Contributor

Background

We all are used to having undo redo functionality in applications which we use everyday. Undo redo can be found in text editors, email programs, graphics editors, most of the online tools etc. Wouldn’t it be nice to have it also in our shiny new business applications? Wouldn’t that be one essential part of the user experience?

Here it is. Undo redo functionality for UI5 JSONModels:

Implementation

Before coming up with the idea of this undo redo implementation, I was thinking what could be a good use case for a reusable UI5 Faceless component (class: sap.ui.core.Component)


Faceless components do not have a user interface and are used, for example, for a service that delivers data from a back-end system.






That’s when I understood a nice use case. Attach it with a Model so that changes made to the Model can be kept track of.

How should we continue with the implementation, then? There were basically two options I could think of:

  1. Bind the Faceless component directly into the Model and listen when it dispatches change events.
  2. Wrap the Model with another construct so that every time we change any value in the model it goes through our wrapper.

I first pondered with the first option as it would’ve been more comprehensive. No matter how the model would have been changed, we could have tracked the changes. Unfortunately, this was not an option, as we cannot bind a Faceless component to the Model directly (see http://scn.sap.com/thread/3492009 for possible solution).

Luckily the second option proved to be possible. One downside, of course, is that we have to make every change to the Model using our wrapper. But once you take it, it’s now nice as we have single point of entry for methods modifying our model.

Below you can find the code for undo redo component.


jQuery.sap.declare("util.undoRedo");
      util.undoRedo = (function(jsonModel){
    
        var _jsonModel = jsonModel;
        var _undoStack = [];
        var _redoStack = [];
        var updateModelValue = function(sPath, oValue, oContext) {
          _jsonModel.setProperty(sPath, oValue, oContext);
        };
        return {
          setProperty: function(sPath, oValue, oContext) {        
            var oldValue = _jsonModel.getProperty(sPath);
            updateModelValue(sPath, oValue, oContext)
            window.console&&console.log("Updating stack with newvalue " + oValue + " old value " + oldValue);
            // TODO also keep track of oldContext and newContext
            // TODO should propably make a deep copy of oValue so that tracking works for objects also
            // var newValue = JSON.parse(JSON.stringify(oValue));
            // add new entry into the undoStack
            _undoStack.unshift({"path": sPath, "oldValue": oldValue, "newValue": oValue});
            window.console&&console.log("Undo stack is now: " + JSON.stringify(_undoStack));
            // clear redo stack if needed
            if (_redoStack.length > 0) {
              _redoStack = [];
            }
          },
          undo: function() {
            var undoItem = (_undoStack.length > 0) ? _undoStack.shift() : null;
            if (undoItem !== null) {
              window.console&&console.log("Undo item is " + JSON.stringify(undoItem));
              updateModelValue(undoItem.path, undoItem.oldValue);
              // add item to redoStack
              _redoStack.unshift(undoItem);
            }        
          },
          redo: function() {
            // TODO get item from redoStack
            var redoItem = (_redoStack.length > 0) ? _redoStack.shift() : null;
            if (redoItem !== null) {
              window.console&&console.log("Redo item is " + JSON.stringify(redoItem));
              updateModelValue(redoItem.path, redoItem.newValue);
              // add item to undoStack
              _undoStack.unshift(redoItem);
            }
          },
        };
    });


It is using module pattern to hide implementation details. I won’t go through all the code verbally as I think that code is best explained by itself. Just a few notes. Component keeps changes in objects that are saved in two stacks, one for undo and another for redo. Change objects contain info about path, oldValue and newValue. In undo value in the path is replaced with oldValue and in redo value is replaced with newValue. Changes are saved as objects so saving possibly only works for simple types. Saving objects or arrays could be possible if change data was kept in json format (see TODO in code).

Here’s how to use the code with two different examples:


jQuery(function() {
        // create model
        var modelJson = {"textVal" : "initial value"};
        var model = new sap.ui.model.json.JSONModel(modelJson);
        sap.ui.getCore().setModel(model);
        // initialize undo redo component and wrap JSONModel with it
        var undoRedoComponent = util.undoRedo(model);
        var textInput = new sap.m.Input({
          //value : "{/textVal}",
          change : function(evt) {
            var newVal = evt.mParameters.newValue;
            // change value on Model through undoRedo so that it can keep track of changes
            undoRedoComponent.setProperty("/textVal", newVal);
          }
        });
        // BindingMode should be Oneway because undoRedo does not listen to changes in the model directly
        textInput.bindValue("/textVal", null, sap.ui.model.BindingMode.OneWay);
        var hbox = new sap.m.HBox();
        var undoButton = new sap.m.Button({text: 'Undo',
          press : function() {
            undoRedoComponent.undo();
          }});
        var redoButton = new sap.m.Button({text: 'Redo',
          press : function() {
            undoRedoComponent.redo();
          }});
    
        hbox.addItem(undoButton);
        hbox.addItem(redoButton);
    
        textInput.placeAt('content');
        hbox.placeAt('content');
      });



jQuery(function() {
        // create model
        var beers = [
          {"style": "Imperial Stout", "name": "Plevna Siperia", "rating": 3},
          {"style": "Weissbier", "name": "Paulaner Naturtrüb", "rating": 3},
          {"style": "India Pale ale", "name": "Founders Centennial IPA", "rating": 3},
          {"style": "Trappist", "name": "Orval", "rating": 3},
        ];
        var modelJson = {"listItems": beers};
        var model = new sap.ui.model.json.JSONModel(modelJson);
        sap.ui.getCore().setModel(model);
        // initialize undo redo component and wrap JSONModel with it
        var undoRedoComponent = util.undoRedo(model);
        // TABLE STARTS HERE
        //Create an instance of the table control
        var oTable = new sap.ui.table.Table({
          title: "Beers table",
          visibleRowCount: 7,  
          firstVisibleRow: 3,
          columnHeaderHeight: 30,
          selectionMode: sap.ui.table.SelectionMode.Single,
          toolbar: new sap.ui.commons.Toolbar({items: [
            new sap.ui.commons.Button({text: "Undo", press: function() {
              undoRedoComponent.undo();
              // we refresh the model to show changes
              model.refresh(true);
            }})
          ]}),
          extension: [
            new sap.ui.commons.Button({text: "Redo", press: function() {
              undoRedoComponent.redo();
              // we refresh the model to show changes
              model.refresh(true);
            }})
          ]
        });
        //Define the columns and the control templates to be used
        var oColumn = new sap.ui.table.Column({
          label: new sap.ui.commons.Label({text: "Name"}),
          template: new sap.ui.commons.TextView().bindProperty("text", "name"),
          sortProperty: "name",
          filterProperty: "name",
          width: "200px",
          editable: false
        });
        var oColumn2 = new sap.ui.table.Column({
          label: new sap.ui.commons.Label({text: "Style"}),
          template: new sap.ui.commons.TextView().bindProperty("text", "style"),
          sortProperty: "style",
          filterProperty: "style",
          width: "200px",
          editable: false
        });
        // BindingMode should be Oneway because undoRedo does not listen to changes in the model directly
        var oColumn3 = new sap.ui.table.Column({
          label: new sap.ui.commons.Label({text: "Rating"}),
          template: new sap.ui.commons.RatingIndicator({
            maxValue: 5,
            change: function(evt) {          
              var value = evt.mParameters.value;
              var bindingContext = evt.getSource().getBindingContext();
              window.console&&console.log("binding context is " + bindingContext);
              undoRedoComponent.setProperty(bindingContext.sPath + "/rating", value);
            }
          }).bindValue("rating", null, sap.ui.model.BindingMode.OneWay),
          sortProperty: "rating",
          filterProperty: "rating",
          width: "200px"
        });
        var oCustomMenu = new sap.ui.commons.Menu();
        oCustomMenu.addItem(new sap.ui.commons.MenuItem({
          text:"Custom Menu",
          select:function() {
            alert("Custom Menu");
          }
        }));
        oColumn.setMenu(oCustomMenu);
        oTable.addColumn(oColumn);
        oTable.addColumn(oColumn2);
        oTable.addColumn(oColumn3);
        oTable.bindRows("/listItems");
        oTable.placeAt('content');
      });


Most important thing to note here is that undoRedo component does not work with two-way binding. That is because it cannot keep track of changes made to the Model using View to Model change. This is similar to how formatters work so I don’t see this as a big problem.

Todo

  • Make undoRedo work with Objects. I’ve only tested it with simple types
  • Allow adding items into and removing them from arrays in a Model. Example use case: add a row into a table
  • Set maximum size of undo stack to limit memory usage in production applications
  • Make undoredo component listen to all changes made to the Model even if they were made directly

Live examples

Example 1: http://jsbin.com/weboqi/1/edit

Example 2: http://jsbin.com/norani/1/edit

Thanks for reading!

7 Comments
Labels in this area