AutoSavePrime


This is a slim, zero-dependency, heavily unit-tested, production-ready library that AutoSaves users' work as they go. It conforms to new HTML5 form semantics but also has been built from the ground up to ensure it works on olders versions of browsers (e.g. IE6+) and is also compatible with other libraries like jQueryUI and CKEditor (see examples). It's meant as a serious library where auto-saving may be a crucial element of the user experience.

The library auto-saves all input controls as they're entered. If the user navigates away and comes back, the data will be restored and they can carry on working. There are several options for where the data is stored. There's also a number of demos. The GitHub repo is here.

Backwards compatability is a core feature of this library and future versions will always be built upon the existing API; so you can develop knowing that you can safely upgrade in future without (much) breakage. Let's get started !

QuickStart

Like most modules, this is defined as a UMD so works with all module loaders and independently too. See this demo.

No Module Loader
...
	<head>
		<script src="https://cdn.jsdelivr.net/npm/autosave-prime@1.0.2/dist/AutoSave.min.js"></script>
		<script type="javascript"> 
			var autoSave = new AutoSave()
		</script>
	</head>
...
Using RequireJS

First install with npm install autosave-prime

...
		<script type="javascript"> 
			require([ 'autosave-prime' ],
				function( AutoSave ){
					var autoSave = new AutoSave();
				}
			);
		</script>
...
Using TypeScript

First install with npm install autosave-prime (also see the complete Typescript Setup in the demos section)

import AutoSave = require("../node_modules/autosave-prime/dist/AutoSave");
...
		class MyFormManager {
			constructor(){
				var opts: AutoSave.InitOptions = {
					onLog: (level)=>level == AutoSave.LOG_ERROR, //Disable all non-error logging
				};
				new AutoSave(null, opts);
			}
		}
That's it ! Use it on your own page or take a look at the basic demo.

Initialisation Options

AutoSavePrime can of course be customised. Most of this is through the initOptions parameter when the AutoSavePrime instance is initialised, which includes several callbacks that can act as either passive listeners or interceptors to stop or modify an operation.

new AutoSave( [ scope, initOptions ] );

Parameter: scope

AutoSavePrime by default will assume the whole document needs to be auto-saved if this parameter is falsy. The scope parameter can be used to limit what's being saved.

The scope parameter can be a single native element or an array of them, a string selector, a jQuery instance, or a function that returns any of these. It does not need to be a form or even contain a form.

All input elements underneath scope elements will be eligible for auto-saving.

 //These are all supported
new AutoSave();
new AutoSave( elem );
new AutoSave( [ elem1, elem2, elem3 ] );
new AutoSave( "#container1, #container2" );
new AutoSave( $("#container") ); //We recognise jQuery
										

The scope elements must be present in the DOM at the time the AutoSave instance is created and will not be dynamically recalculated after initialisation - i.e. they should be the container(s) for any dynamically created controls. Also see onPreSerialize and this demo

Parameter: initOptions

Full listing
var opts: {

  autoSaveTrigger: [ null |
                     {
						debounceInterval: number	//Default: 3000
					 }
				   ],
  autoLoadTrigger: null,
  seekExternalFormElements: boolean,				//Default: true
  saveNotification: [ null | 
                      {
                        template: string,
                        message: string,
                        minShowDuration: number		//Default: 500
                      }
                    ],
  noStorageNotification: [ null | 
                           {
                             template: string,
                             message: string,
                             showDuration: number	//Default: 5000
                           }
                         ],
  dataStore: [ null |
			   {
			     preferCookies: boolean,			//Default: false
				 clearEmptyValuesOnLoad: boolean,	//Default: true
				 key: [ string | func ],			//func: ()=>string
			 	 save: function, 					// :(key, data, saveCompleteCallback)=>void
			 	 load: function 					// :(key, loadCompleteCallback)=>void
			   }
		     ],
			 
			 
  //@FUNC-compliant callbacks
  onInitialised: function, 		 	// :()=>void
  onPreLoad: function,				// :()=>void|boolean|string
  onPostLoad: function,				// :(string)=>void|boolean|string
  onPostDeserialize: function,		// :()=>void
  onPreSerialize: function,			// :(HTMLElement[])=>void|boolean|string|HTMLElement|HTMLElement[]|jQuery
  onPreStore: function,				// :(string)=>void|boolean|string
  onPostStore: function,			// :()=>void
  onSaveNotification: function,		// :(boolean)=>void|boolean
  onNoStorageNotification: function,// :(boolean)=>void|boolean
  onLog: function|object			// :((string, ...any[])=>any)|object //i.e. a variadic function
}
										

Note: All parameters are optional.

Details
  • autoSaveTrigger

    AutoSavePrime listens to input changes on elements under the scope and debounces them before making a save request. The default debounce interval is 3000ms. You can customise this interval by passing an object with the debounce interval set (in milliseconds). Setting this property to null disables auto-saving altogether but you can save manually at any time by invoking autoSave.Save().

    //Example usage
    var autoSave = new AutoSave( null, {
    	autoSaveTrigger: {
    		debounceInterval: 10*1000 //Changes will be debounced over 10 seconds now before save
    	}
    })													
  • autoLoadTrigger

    AutoSavePrime will populate a form with details from the datastore on page load (after the document is ready). Setting this property to null will disable this behaviour altogether but you can load manually at any time by invoking autoSave.Load()

  • seekExternalFormElements

    For all Form elements specified at or under your scope elements, AutoSavePrime will seek input elements elsewhere in the document that are linked to this form. Setting this property to false will disable this behaviour.

  • saveNotification

    When a save is kicked off, a notification briefly shows at the top indicating a save is in progress. By default, it's got a simple non-distracting look but see the demos for styling it :-

    By specifying null, the notification is disabled

    message:Setting this property overrides the message to display in the notification bar.

    template:This must be a valid HTML fragment that replaces the entire notification container. The existing container can be styled with css but incase more drastic changes are required this allows you to do that.

    Either message or template can be specified but not both.

    minShowDuration:The save process normally happens instantly, especially when using local storage. To allow the user to see the notification that their work is being auto-saved, the notification has a minimal time for which it shows. By default this is 500ms (but it can show for longer if the save is taking longer - over a network, say). The minimum duration can be changed by setting this parameter (in milliseconds).

    //Example usage #1 - simple message customise
    var autoSave = new AutoSave( null, {
    	saveNotification: {
    		message: "Conservé...", //The same message in French
    		minShowDuration: 2000 //Override to show for at least 2 seconds
    	}
    })													
    /* Use this CSS to customise the notification */
    .autosave-saving .autosave-msg{
    	text-decoration: underline;
    }													
    //Example usage #2 - custom HTML template
    var autoSave = new AutoSave( null, {
    	saveNotification: {
    		template: "<div id='my_custom_notification'><span>My own message that autosave is saving</span></div>",
    		minShowDuration: 2000 //Override to show for at least 2 seconds
    	}
    })													
  • noStorageNotification

    When AutoSavePrime detects that neither cookies nor local storage is available a notification briefly shows at the top stating auto-save is disabled. If the datastore however has been customised with load and save callbacks, the notification never shows. By default, it looks like the below but see the demos for styling it :-

    message:Setting this property overrides the message to display in the notification bar.

    template:This must be a valid HTML fragment that replaces the entire notification container. The existing container can be styled with css but incase more drastic changes are required this allows you to do that.

    Either message or template can be specified but not both.

    showDuration:The notification by default shows for 5 seconds but can be overriden by this setting. Set in milliseconds.

    //Example usage #1 - simple message customise
    var autoSave = new AutoSave( null, {
    	saveNotification: {
    		message: "AutoSave est éteint", //The same message in French
    		showDuration: 3500 //Show for exactly 3.5 seconds
    	}
    })													
    /* Use this CSS to customise the notification */
    .autosave-noStore .autosave-msg{
    	text-decoration: underline;
    }													
    //Example usage #2 - custom HTML template
    var autoSave = new AutoSave( null, {
    	saveNotification: {
    		template: "<div id='my_custom_notification'><span>My own message about autosave unavailability</span></div>",
    		showDuration: 3500 //Show for exactly 3.5 seconds		
    	}
    })													
  • dataStore

    AutoSavePrime by default will try and use the HTML5 local storage API for data storage. If this isn't detected, it'll fallback to using cookies. You can also customise the datastore with custom functions to, for example, save and load to/from a server instead.

    By specifying null, saving and loading is disabled. You can instead use the hooks to drive saving and loading.

    preferCookies: Even if local storage is available, you can switch to using cookies by setting this option to true.

    clearEmptyValuesOnLoad: By default, on load AutoSavePrime will clear existing form entries if an entry value is specified in the payload. This is useful if you do a server-side reset of the user's controls and want to blank them out. e.g. If the loading payload has name=&age=, the inputs would be blanked out on a load regardless of their existing values. However, setting this property to false will cause the blank values to be ignored and the inputs to retain their existing values.

    key: As you can have multiple AutoSavePrime instances on one page, this property is used to distinguish between them. The key is a string used to index into the datastore and is automatically generated by the formula

    key = "AutoSaveJS_" + URL.path + Form.Name

    If there isn't a Form with a Name property in the scope set, then of course the last term above isn't applied.

    Sometimes you will need to set this key explicitly - namely when

    1. You have multiple AutoSavePrime instances on one page and they don't have a Form element with a name property in the scope set (Example #1 below)
    or
    2. You have multiple pages using AutoSavePrime and the URL path is the same for all (Example #2 below).

    <!-- Example Context -->
    <div id="container_1">
    	...
    </div>
    <form id="container_2">
    	...
    </form>
    <form id="container_3" name="contact_details">
    	...
    </form>
    1. Multiple instances on one page
    //1a) (URL: http://www.example.com/customer_entry) new AutoSave( document.querySelector( "#container_1" ) ); //Key generated: AutoSaveJS_customer_entry new AutoSave( document.querySelector( "#container_2" ) ); //Error: Key already in use //1b) (URL: http://www.example.com/customer_entry) new AutoSave( document.querySelector( "#container_1" ) ); //Key generated: AutoSaveJS_customer_entry new AutoSave( document.querySelector( "#container_2" ), { dataStore: { key: "extra_details" } }); //Key generated: AutoSaveJS_customer_entry_extra_details //1c) (URL: http://www.example.com/customer_entry) new AutoSave( document.querySelector( "#container_1" ) ); //Key generated: AutoSaveJS_customer_entry new AutoSave( document.querySelector( "#container_3" ) ); //Uses the form name //Key generated: AutoSaveJS_customer_entry_contact_details
    2. Multiple Instances across pages
    //2a. (URL: http://www.example.com/customer_entry?customer_id=10) //Query parameters are ignored new AutoSave( document.querySelector( "#container_1" ) ); //Key generated: AutoSaveJS_customer_entry // (URL: http://www.example.com/customer_entry?customer_id=11) new AutoSave( document.querySelector( "#container_1" ) ); //Error: Key already in use //2b. (URL: http://www.example.com/customer_entry?customer_id=10) new AutoSave( document.querySelector( "#container_1" ) ); //Key generated: AutoSaveJS_customer_entry new AutoSave( document.querySelector( "#container_1" ), { dataStore: { key: "cust_10" } }); //Key generated: AutoSaveJS_customer_entry_cust_10
    Finally, you can also customise the key handling entirely by the onPreLoad and onPreStore interceptors. These allow you to, for example, share a datastore across multiple AutoSavePrime instances on different pages.

    load, save: You can change the datastore to a completely custom one by specifying functions for the load and save. The key parameter supplied in both helps identify which instance is being saved. For example

    var autoSave = new AutoSave( null, {
    	dataStore: {
    		save: function( key, data, saveCompleteCallback ){
    			mySvc.save( key, data ).then( function(){ //Assume mySvc.save() returns a Promise
    				saveCompleteCallback();
    			});
    		},
    		
    		load:function( key, loadCompleteCallback ){
    			mySvc.load( key ).then( function( result ){ //Assume mySvc.load() returns a Promise
    				loadCompleteCallback( result );
    			});
    		}
    	}
    })													

    The saveCompleteCallback and loadCompleteCallback must be invoked at completion, regardless of being an sync or async operation, else you may get unexpected behavior.

    You must set both the save and load callbacks or neither of them - you cannot set just one.


  • The remaining options are interceptor-cum-inspector callbacks that conform to ...

    @FUNC Semantics

    @FUNC semantics in AutoSavePrime give a consistent experience among callbacks. Concretely, it means that when you register a function callback, what you return dictates what happens next. Your callback could

    Return False. That particular operation will stop dead there (and clean up anything if necessary).

    Simply not return (i.e. Undefined), in which case it'll act as just a passive inspector and the flow will continue as normal.

    Return Null or a Custom data structure so the operation continues with your overriden/enhanced data.

    The supported return values are indicated next to each callback below.

  • onInitialised (U)

    Callback to listen to when AutoSave is fully initialised including the initial load of any existing data from the datastore.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onInitialised: function(){
    		//Do post-initialisation work here
    	}
    })													
  • onPreLoad (FUNC)

    Callback to listen to when AutoSave is about to load data from the datastore. By returning false, you can cancel the load. By returning a string, you can specify the payload to load. This must be encoded as x-www-form-urlencoded.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onPreLoad: function(){
    		if ( some_condition )
    			return false; //prevent load from happening (you can call autoSave.load() afterwards too)
    		else
    			return AutoSave.serialize( "name=Mozart&type=Classical+Music" ); //x-www-form-urlencoded
    	}
    })													

    Tip: You can use AutoSave.serialize to generate the encoded string.

  • onPostLoad (FUNC)

    Callback to listen to after AutoSave has loaded the string from the data source but before deserialising it into the input elements. By returning false, you can cancel the subsequent deserialisation. By returning a string, you can override the payload to be deserialised.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onPostLoad: function( serialisedData ){
    		if ( some_condition )
    			return false; //Cancel the load
    		else if ( other_condition )
    			return serialisedData + "&firstName=James" //Append an extra field to be loaded
    		
    		//Otherwise, continue loading as normal
    	}
    })													
  • onPostDeserialize (U)

    Callback to listen to after AutoSave has deserialised the string into the input elements.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onPostDeserialize: function(){
    		//All inputs loaded - you could now, for example, initialise UI libraries like CKEditor
    	}
    })													
  • onPreSerialize (FUNC)

    Callback to listen to before all the input elements are serialised into one long x-www-form-urlencoded string. By returning false, you can cancel the subsequent serialisation (and hence the save). The set of elements can be customised by returning any of the types shown above.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onPreSerialize: function( controlsArray ){
    		if ( some_condition )
    			return false; //Cancel the serialisation - e.g. check if any inputs are invalid
    		else
    			return controlsArray.slice( 0, Math.min(3,controlsArray.length) ); //Only serialize the first 3 controls
    	}
    })													
  • onPreStore (FUNC)

    Callback to listen to before the serialised string is stored to the datastore. By returning false, you can cancel the save. The serialised string can be modified and returned.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onPreStore: function( serialisedString ){
    		if ( some_condition )
    			return false; //Cancel storing it
    		else
    			return serialisedString + "&timestamp="+mySvc.getTimestamp() //Append an extra field to be saved down
    	}
    })													

    For cookie-based storage, the string also contains the cookie parameters - like the expiry date - so this would be a good place to customise the final cookie string.

  • onPostStore (U)

    Callback to listen to after the serialised string is stored to the datastore.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onPostStore: function(){
    		//Save completed - you could now, for example, re-enable controls to continue working
    	}
    })													
  • onSaveNotification (FU)

    Callback to listen to just before the saving notification visibility is toggled. The parameter is true if the notification is about to be shown and false just before its hidden. Return false to cancel showing the notification.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onSaveNotification: function( isToggleOn ){
    		if ( some_condition ) {
    			return false; //Don't show the notification
    		}
    		else {
    			//Disable save button whilst auto-saving
    			document.querySelector( "button[type='submit']" ).enabled = !isToggleOn;
    		}
    	}
    })													
  • onNoStorageNotification (FU)

    Callback to listen to just before the notification that there is no storage available is toggled. The parameter is true if the notification is about to be shown and false just before its hidden. Return false to cancel showing the notification.

    //Example usage
    var autoSave = new AutoSave( null, {
    	onNoStorageNotification: function( isToggleOn ){
    		if ( isToggleOn ) {
    			//If local storage isnt available, switch to using a server-side implementation
    			mySvc.initialiseAjaxService();
    		}
    	}
    })													
  • onLog (FUNC)

    Callback for handling logging. If a function is supplied, it must be variadic with the signature

    // (level: string, ...logArgs: any[]) => any

    where level will be one of the following strings

    • debug (AutoSave.LOG_DEBUG)
    • info (AutoSave.LOG_INFO)
    • warn (AutoSave.LOG_WARN)
    • error (AutoSave.LOG_ERROR)

    The argument returned from your callback is what gets sent to the log sink instead of the original arguments. If false is returned, this is a special case where the logging is aborted.

    For convenience and easy integration with other libraries, you can also pipe messages at specific levels by specifying an object for this parameter. Both methods are shown in the following example

    //Example usage - with 1 function
    var autoSave = new AutoSave( null, {
    	onLog: function( level, _variadic_args_ ){
    	
    		//Log errors to svc in production
    		if ( mySvc.isProduction() ) {
    			if ( level == AutoSave.LOG_ERROR )
    				mySvc.logError( arguments.slice( 1 ) ); //Skip the level parameter
    		}
    
    		return false; //Don't write any logs to the default output (console)
    	}
    })
    
    //Example usage - with level piping functions
    var autoSave = new AutoSave( null, {
    	onLog: {
    		debug: function( _variadic_args_ ){
    			//Never pipe debug logging to downstream logger
    			return false;
    		}
    		info:  mySvc.logger.info,
    		warn:  mySvc.logger.warn,
    		error: mySvc.logger.error
    	}
    })													

    All functions are optional and for any levels without a function, default logging will take place.

    Returning false from any of these will prevent default logging (to console). Anything else returned overwrites the existing log message. If an array is returned, it will become a variadic parameter list to the log sink.

    See the demos for how to seamlessly wire these into any existing logging you have.

Instance Members

Full listing
//Functions
autoSave.getCurrentValue()	//: ()=>string
autoSave.save()				//: ()=>void
autoSave.load()				//: ()=>void
autoSave.resetStore()		//: ()=>void
autoSave.dispose(resetStore=true) //: (boolean)=>void
										
Details
  • autoSave.getCurrentValue()

    Returns a string resulting from the serialisation of all the inputs in the scope of this instance in x-www-form-urlencoded format.

  • autoSave.save()

    Triggers a save of the input elements to the datastore. If there's already a save in progress, this will get queued to run after that's finished. See datastore for more details.

  • autoSave.load()

    Manually triggers a load of the input elements from the datastore. Existing data may be wiped (see also clearEmptyValuesOnLoad).

  • autoSave.resetStore()

    Empties the datastore for this instance. Specifically, for the key-value pair stored, it'll set value to null but will keep the key marked as in-use.

  • autoSave.dispose(resetStore=true)

    Unhooks all listeners, removes notification elements, terminates all debounce-pending saves, releases the datastore key and generally cleans up after itself. By default, will remove all data in local storage associated with this instance but set resetStore to false to override this behaviour and preserve data in the datastore.

Static Members

Most of the logic in AutoSavePrime is in stateless functions to minimise the places shared state is read/updated. A number of these stateless helpers are available for you should you need them in any custom logic in callbacks etc.

Full listing
//Functions
AutoSave.isCookieStorageAvailable()	//: ()=>boolean
AutoSave.isLocalStorageAvailable()	//: ()=>boolean
AutoSave.serialize(controlsArr)		//: (HTMLElement[])=>string
AutoSave.deserialize(controlsArr, data, clearEmpty=true) //: (HTMLElement[], string[, boolean])=>void
AutoSave.resetAll()					//: ()=>void
AutoSave.toArray(data, numSkip=0)	//: (array-like[, number])=>any[]
AutoSave.whenDocReady(myCallback)	//: (()=>void)=>void

//Constants
AutoSave.LOG_DEBUG 	//: string ("debug")
AutoSave.LOG_INFO 	//: string ("info")
AutoSave.LOG_WARN 	//: string ("warn")
AutoSave.LOG_ERROR 	//: string ("error")
AutoSave.version 	//: string
										
Details
  • AutoSave.isCookieStorageAvailable()

    Returns true if the browser has cookies enabled.

  • AutoSave.isLocalStorageAvailable()

    Returns true if the browser has the HTML5 Local Storage API and it's enabled.

  • AutoSave.serialize(controlsArr)

    Given an array of elements, serializes the values of all the input elements at or underneath this set and returns it as a x-www-form-urlencoded string representation.

  • AutoSave.deserialize(controlsArr, data, clearEmpty=true)

    Loads all the input elements that are in the controlsArr array with values from the data string. The data string must be in x-www-form-urlencoded encoding. If there are any empty parameters, like 'name=&...', then the name input field will be cleared out. Setting the clearEmpty parameter to false will prevent this behaviour.

  • AutoSave.resetAll()

    Removes all local storage and cookies generated by any AutoSave instances.Will remove everything on this domain*. Will not dispose currently active AutoSave instances. *Except cookies that have been customised by custom domain or path parameters through the onPreStore callback.

  • AutoSave.toArray(arrayLike, numSkip=0)

    Given an array-like object (i.e. any object with a length property and indexed properties), creates a first class javascript array from it. You can optionally specify a numSkip number to skip a given number of entries at the start.

  • AutoSave.whenDocReady(myCallback)

    Will invoke myCallback when the document's readyState transitions to 'complete' - the equivalent of jQuery's $(). myCallback should have type of ()=>void.

Demos

  • Basic Example
  • Loading with RequireJS
  • Typescript Setup
  • Server-side saves with only delta
  • Customising the scope set
  • Incorporating form elements outside container (HTML5)
  • Creating and destroying multiple AutoSave instances
  • Customising data being stored and loaded
  • Logging in Production vs Debug
  • Cancelling Save until all inputs validated
  • Dynamically Created Controls
  • Localisation
  • Using With jQuery UI
  • Using With UniformJS
  • Using With simplelog and winstonlog
  • Using With CKEditor
  • Extended example - multi user scenario with server-side saving

Development

This section is for developers who want to work deeper with the library or fork/extend/contribute to it.

Exception handling
This is designed so any error can always be caught, even if it's caused by an event at a much later stage than initialisation. If an error is raised in the library which was initiated by a user call, it'll bubble up to the user. Otherwise, it'll be piped through the onLog error handler.

A robust error handling strategy then would look like this :-

try {
	new AutoSave(null, {
		onPreSerialize:function() {
			try {
				myService.audit("Serialising...");
			}
			catch(ex){/* ... */} //Custom callbacks need to handle their own errors
		},
		onLog:(level){ if (level == AutoSave.ERROR){/* ... */}} //For browser-initiated events that cause exceptions. If no handler is supplied, will be piped to the console.
	});
} catch(ex){/* ... */} //An exception can be raised during construction

...

try {
	autoSave.load(...)
} catch(ex){/* ... */} //User-initiated actions could throw
Build Scripts

- You can execute 'npm run lint', which will use jshint to catch any potential issues.

- You can execute 'npm run package', to minify and bundle all resources necessary for publishing into dist/.

- You can see the status of the ~130 Jasmine+Karma unit tests locally by going to ~/tests/SpecRunner.html