Playing with the kavanot editor, I wanted some way to encapsulate the process of noting that the text is "dirty"—meaning has been changed but not yet saved, and using a Promise to express the idea that the text is soon-to-be-saved, then saved.

I created a plugin that simply represents a state machine with four states, clean, dirty, pending, and failed. So you can do

var monitor = $.savemonitor();
$('textarea').on('input', function() { monitor.dirty() }); // track when the text area changes
// call this function to save (say, as a click handler for a Save button
function save(){
  var text = $('textarea').val();
  monitor.clean($.post('saveit.php', {data: text})); // jQuery Deferred's like $.post can be cast to Promises
// and keep track of the status
monitor.on('clean', function() {console.log('saved')})
monitor.on('failed', function() {console.log('not saved')})

See the code on github.

See a simple demo.

See a demo that uses FontAwesome icons and randomly fails to simulate a network failure.


var monitor = $.savemonitor([indicator]);

creates a monitor object with the following methods:

setter(newstate {String})
Returns a function that sets the state to newstate and triggers an event with the same name. Uses some basic logic for the state transitions:
  1. Doesn't trigger events if the state isn't changing
  2. Only allows the four states listed above
  3. The only transition from 'dirty' is to 'pending'. That avoids the problem of starting to save (state is 'pending'), then editing (state is 'dirty') then getting the confirmation that the save happened (state would be 'clean' but the save is out of date).
  4. The only transition from 'clean' is to 'dirty', so we don't try to save an already saved version and possibly get to 'failed' even though the last save is up to date.
Note that this returns a function to change the state; it does not actually change it. This makes it appropriate for creating a callback (like $('textarea').on('input', monitor.setter('dirty'));.
Calls setter('dirty')(), so it changes the state and triggers the event.
Calls setter('pending')(), then Promise.cast(resolver).then(this.setter('clean'), this.setter('failed')), so it takes advantage of the Promise.cast method to handle error handling. If resolver is undefined or anything without a then() method, it immediately resolves and calls setter('clean')(). If resolver is what the Promises API calls a "thenable" (like a real Promise, or a jQuery Deferred) then waits for that to resolve and calls setter('clean')() or setter('failed')() as appropriate.
on(event {String}, handler {function})
Sets up an event handler for the given event (will be one of clean, dirty, pending, and failed). Just uses $(this).on(event, handler), so you can use multiple event names, event namespaces, etc.
off(event {String}, handler {function})
Removes the event handler for the given event. Uses $(this).off(event, handler).
Triggers the event for the current state.
Returns the current state.

If the indicator parameter to $.savemonitor(indicator) is set, then $(indicator) is used as a target for the event changes, changing its class to the current state, with

monitor.on('clean dirty failed pending', function (event){
  $(indicator).removeClass('clean dirty failed pending');

so you can just use CSS to display the state, with no code at all.


There's also a way to attach a monitor to an element directly, with var monitor = $(element).savemonitor(indicator). This stores the monitor object in $(element).data('savemonitor') so it won't be created more than once. It returns the monitor object, so this plugin can't be chained. It adds an event handler for the input event to track changes exactly as above, with $(element).on('input', monitor.setter('dirty') so no other code is necessary.

The simplest use is just two lines of code:

  button::after {
    content: ' (' attr(class) ')'; /* to show the state */
var monitor = $('textarea').savemonitor('button');
$('button').click(function() {monitor.clean( $.post('saveToServer.php', {data: $('textarea').val()})) });

Leave a Reply

Warning: Undefined variable $user_ID in /home/public/blog/wp-content/themes/evanescence/comments.php on line 75