Make client undo manager persistent as well as anyone of JavaScript objects

Here we would like to describe the possibility of creation of persistent JavaScript objects on client's side. Let's take undo manager as an example. It will demonstrate that this uniform solution expands the field of application of the same JavaScript objects.

Undo manager

Let's recognize the first part of our article as �pragmatic�. It is devoted to the solution of particular task - namely the creation of undo manager on client's side. Main purpose of undo manager is a recorder of undo and redoes operations. Undo manager allows users to correct their mistakes and also to try out different aspects of the application without risk of repercussions.

Undo-redo mechanism provides such abilities as:

  • Un-execute (undo) the last action they just performed.
  • Re-execute (redo) the last undone action.

Proposed approach of undo manager provides multi-level undo support and based on undoable actions, which contains logic how to do something and how to get back to the starting point. The undo manager records commands on a stack. When the user undoes a command, the undo manager pops it from the stack, revealing the previously executed command. Once a user has undone at least one command, executing any new command clears the redo stack.

If you choose, you can also force the undo manager to provide predefined level of undo support, where it remembers only predefined count of commands.

One of the strong features of undo manager is customizable error processing which allows to manager be fault tolerant or fault restore in accordance with particular logic.

Undoable action

As stated above, undo manager records undoable actions. Undoable Action is such kind of action, which is not only aware of how to do something, but also how to get back to the starting point. Undoable action consists of two unfold operations: DO and UNDO.

Proposed implementation of undoable Action wraps two JavaScript methods, one of them represents DO operation, another - UNDO operation and allows define corresponding parameters to invoke them.

For example, let there exist some variable and an action, which increases this variable by 2. Let's name this action DO. The action, which corresponds to the UNDO, wills decrement the variable by 2.

According to the most pessimistic estimation, undoable action can be represented as a set of object's state saving operations, object's change at the DO command, and object's renovation at the UNDO command.

But generally, undoable action is wrapped two functions or of two functions with corresponding parameters.

The following listing illustrates undoable action:

/**
 * Creates undoable Action, such kind of action, which is not only
 * aware of how to do something, but also how to get back to the starting point.
 * Undoable action consists of two unfold operations: DO and UNDO.
 * Proposed implementation of undoable Action wraps two JavaScript methods, one of
 * them represents DO operation, another - UNDO operation
 * but really undoable Action is an interface and can be implemented
 * corresponding to particular logic.
 *
 * @constructor
 * @param {Function} The undoable Action DO method.
 * @param {Function} The undoable Action UNDO method.
 * @param {Array} The array of parameters of DO method.
 * @param {Array} The array of parameters of UNDO method.

 * @param {String} The undoable Action presentation.
 * @param {String} The undoable Action group.
 */
function UndoableAction(aDoMethod, aUndoMethod, aDoParams, aUndoParams, aName, aGroup)
{
    
this.fStatus undefined;
    this
.fName COMMONS.isString(aName) ? aName : undefined;
    this
.fGroup aGroup;

    this
.fDoMethod aDoMethod;
    this
.fUndoMethod aUndoMethod || aDoMethod;
    this
.fDoParams COMMONS.isUndefined(aDoParams) || COMMONS.isArray(aDoParams) ? aDoParams : [aDoParams];
    this
.fUndoParams COMMONS.isUndefined(aUndoParams) || COMMONS.isArray(aUndoParams) ? aUndoParams : [aUndoParams];
}

/**
 * Creates undoable Action logger.
 */
UndoableAction.prototype.fLogger = new Logger("UndoableAction")

/**
 * The value designates that do method of undoable Action
 * was called successfully.
 */
UndoableAction.STATUS_DO_OK 1;

/**
 * The value designates that do method of undoable Action
 * was called not successfully (same errors occurred).
 */
UndoableAction.STATUS_DO_ERROR 1000;

/**
 * The value designates that undo method of undoable Action
 * was called successfully.
 */
UndoableAction.STATUS_UNDO_OK 2;

/**
 * The value designates that undo method of undoable Action was called
 * not successfully (same errors occurred).
 */
UndoableAction.STATUS_UNDO_ERROR 1001;

/**
 * Returns undoable Action group.
 * Usually action group represents action type and used to group similar actions.
 *
 * @return {String} Returns undoable Action group.
 */
UndoableAction.prototype.getGroup = function()
{
    
return this.fGroup;
};

/**
 * Returns undoable Action name.
 * Usually action name used to create undoable Action presentation.
 * @return {String} Returns undoable Action name.
 *
 * @param {Boolean} The true designates that "DO"  presentation requested, false - "UNDO".
 */
UndoableAction.prototype.getName = function(anUndo)
{
    
return this.fName;
};

/**
 * Returns undoable Action status.
 * The undoable Action status designates result of calling action.
 * If the action never was called, action status is undefined.
 * It value used by undo manager to select logic after action calling.
 *
 * @return {Integer} Returns undoable Action status.
 */
UndoableAction.prototype.getStatus = function()
{
    
return this.fStatus;
};

/**
 * Calls undoable Action DO method.
 */
UndoableAction.prototype.callDo = function()
{
    
if (COMMONS.isFunction(this.fDoMethod))
    {
        
try
        
{
            
if (COMMONS.isArray(this.fDoParams))
            {
                
this.fDoMethod.apply(thisthis.fDoParams);
            
}
            
else
            
{
                
this.fDoMethod.call(this);
            
}
            
this.fStatus UndoableAction.STATUS_DO_OK;
        
}
        
catch(ex)
        {
            
this.fStatus UndoableAction.STATUS_DO_ERROR;
            this
.fLogger.error("doAction, error happened", ex, this.fDoMethod);
        
}
    }
}
;

/**
 * Calls undoable Action UNDO method.
 */
UndoableAction.prototype.callUndo = function()
{
    
if (COMMONS.isFunction(this.fUndoMethod))
    {
        
try
        
{
            
if (COMMONS.isArray(this.fUndoParams))
            {
                
this.fUndoMethod.apply(thisthis.fUndoParams);
            
}
            
else
            
{
                
this.fUndoMethod.call(this);
            
}
            
this.fStatus UndoableAction.STATUS_UNDO_OK;
        
}
        
catch(ex)
        {
            
this.fStatus UndoableAction.STATUS_UNDO_ERROR;
            this
.fLogger.error("undoAction, error happened", ex, this.fUndoMethod);
        
}
    }
}
;

/**
 * Indicates that the action is reversible.
 * Limitation: cross links in parameters are not supported.
 * 
 * @return {Boolean} If so it returns true, otherwise it returns false.
 */
UndoableAction.prototype.isReversible = function()
{
    
var result = this.fDoMethod === this.fUndoMethod && COMMONS.isEquals(this.fDoParams, this.fUndoParams);
    return 
result;
};

/**
 * Merges given action with the current action.
 *
 * @param {UndoableAction} The undoable action which should be merged with current action.
 * @return {Boolean} Returns true if merge operation accessible, otherwise - false.
 */
UndoableAction.prototype.merge = function(aUndoableAction)
{
    
var result = false;
    if 
(aUndoableAction instanceof UndoableAction)
    {
        
if (COMMONS.isDefined(this.fGroup) && this.fGroup === aUndoableAction.getGroup() &&
            COMMONS.isDefined(
this.fDoParams))
        {
            
if (this.fDoMethod === aUndoableAction.fDoMethod && this.fUndoMethod === aUndoableAction.fUndoMethod)
            {
                
this.fDoParams aUndoableAction.fDoParams;
                
result = true;
            
}
        }
    }
    
else
    
{
        
this.fLogger.error("merge, illegal agrument type:" + aUndoableAction);
    
}
    
return result;
};

Additionally, undoable action contains:

  1. Presentation - data, which is used to create client's interface of undo manager.
  2. Action groups - information on action type, which can be used, both for similar integration actions, join actions and vice versa, artificial adding action in stack of command, depending on particular business logic.

Two ways to record undoable action to undo manager are implemented:

  1. Calls undoable action (or not calls - we are not controls it) and adds it to undo stack.
  2. Adds undoable action to undo manager undo stack and automatically calls DO method of the action. If it was successfully adds the action to undo stack, otherwise - calls DO error processing

The following code snippet illustrates usage of Undo Manager

    var undoManager = new UndoManager(64)
                
  var 
action = new UndoableAction(removeMarker, addMarker, [point], [point]);
                    
 
/*
  * Adds undoable Action to undo stack.
  */                    
    
undoManager.addAction(action)// or undoManager.callDo(action);
    /*
     * Calls undo operation if it is available.
     * Gets last action from undo stack and calls action UNDO method.
     * If it was successfully adds action to redo stack,
     * otherwise - calls undo manager UNDO error processing.
     */                    
  
undoManager.callUndo()
    
/*
     * Calls redo operation if it is available.
     * Gets last action from redo stack and calls action DO method.
     * If it was successfully adds action to undo stack,
     * otherwise - calls undo manager DO error processing.
     */
    
undoManager.callRedo();

The following diagram illustrates internals of Undo Manager:

Internal structure of Undo Manager

Conclusion

Complication and diversity of clients' applications causes the need in creation of various undo managers.

Usually, client's side undo manager is:

  • Simpler then similar client-server realization, because it is client's side, where any actions are performed and so, it easier to determine how these actions can be undone on the client's side.
  • Client side undo manager works faster, then similar client-server realization because interchange can be seldom. Client side undo manager is traffic careful.
  • Client side reduces server's duty.

All undo manager code is free, open source and may be used according to Apace License 2.0 for commercial and not commercial usage. Moreover, code is free for any modifications. It may be downloaded as example for JSONER library from SourceForge with live working examples of using: undo manager that stored D&D operations and undo manager that stored form states (without event handlers on every form fields).

Please click on the image below to check example of live Undo Manager:

Live example of Java Script Undo Manager

Undo Manager was used in real life system within onlin journal editing component (as part of online publishing system). Here it had proved concepts of its design and demonstrated that client-side undo manage worked 10 times faster than using client-server approach.

Make your JavaScript object persistent

Let's recognize the second part of our article as �conceptual�, because it is dedicated to the question of creation of persistent JavaScript objects, demonstrated with an example of client side undo manager and here we will show, how to this extends undo manager usage.

Fact of the matter

The considered client side undo manager has one significant drawback- information on the undo manager state is not delivered to sever and refresh page or navigation by pages erase all history of users action, let alone renovation about new session creation�

For full-fledged work with client side one need have some and preferably uniform way of JavaScript objects serialization in the convenient form for network transmission and objects renovation from this mode.

Undoubtedly, one can write methods of saving and restore of objects states for particular task. But undo manager has very complicated structure, which strongly depends on the client's actions. It may be a very complicated task. Besides, creating objects' hierarchy or extending of object's features required to change serialization logic properly. So we need to as it possible universal JavaScript object persistent approach.

JavaScript persistent is necessary, when:

  1. We want to save the state of a part or client's application in such a way, that they are to be renovated in the next session (hibernate) or page navigation.
  2. When we want to fasten client's application working due to decrease in frequency of queries to the server by means of transmission to the server the results of a set of operations or sequence of operations..
  3. We want to decrease server workload.
  4. JavaScript persistent is one of the ways of system's renovation in case of unpredictable situation on the client's side (e.g. GPF browser by memory leak), when the error can be eliminated by means current page refresh.
  5. Lastly JavaScript persistent gives a new looks at JavaScript objects and extends theirs usage cases. For example, persistent navigation model can move to top most often used commands and hide seldom used commands (MS style, personalized menu).

Possible solutions

So, to make JavaScript persistent object one needs to have:

  1. Way of objects saving in the form, which is convenient for network transfer.
  2. Way of objects' renovation from this serialized form.
  3. It is desirable, that the approach was be compatible with JSON protocol (as far as it possible).
  4. Besides, we need a web service, which gives an opportunity to store data in server side by key and transfer them to web client by key too. Key can be chained with current session, user ID, or component ID and component type etc. - corresponding by particular business logic and architecture of partical application.

Undoubtedly, if it was enough to work only with a part of object, which represents only data, the serialization of data as JSON would be the best solution but the objects are not only a set of data, they are contains cross-references, have references or aggregate other objects, has a methods ultimately.

In the article http://www.ajaxline.com/walking-on-javascript-trees dedicated to the tree traversing, we have already mentioned that on the one hand JavaScript object is close on JSON.

Lets expand the standard JSON protocol, in a way it will allow to work with any kinds of JavaScript objects

So, we add:

  • Information on the type of object.
  • The ability to work with object methods and functions, including anonymous functions
  • The possibility to check and restore cross link references
  • Protocol optimization, based on value of default object properties for typified objects.
  • Filter for not supported data
  • Ability to externalize object.

In the article, http://www.ajaxline.com/lazy-inheritance dedicated to the inheritance of objects in JavaScript, we tried to show that typified prototype-based inheritance has several advantages in comparison other variants. Protocol optimization during object serialization confirms it. Inspire if the simplicity of the idea, the task of serialization of objects tree is a difficult task, as JavaScript objects are flexible. But JavaScript object persistent is very helpful solution an in many cases it is the only possible solution.

What additional abilities serialization of objects gives in our example with undo manager?

Persistent undo manager may be considered as saved consequence of transfers of objects from one state to another. And how does it differ from the slide-show or demo-roll? You can load an example at see it.

PS

Any object does not exist in vacuum - object serialization and deserialization have sense only into corresponding context. To transfer serialized object to server don't remember decode special symbols.

Final note about persisting Java Script object

Obviously, that one can describe rather a wide range of tasks, when JavaScript object persistent can ease the application development sufficiently and the main is that it makes the client's application easer and faster, decreases server's and client's duty.



  SourceForge.net Logo   Support This Project