{"id":3164,"date":"2014-01-17T00:54:46","date_gmt":"2014-01-17T06:54:46","guid":{"rendered":"http:\/\/bililite.com\/blog\/?p=3164"},"modified":"2014-01-19T13:07:44","modified_gmt":"2014-01-19T19:07:44","slug":"new-jquery-plugin-savemonitor","status":"publish","type":"post","link":"https:\/\/bililite.com\/blog\/2014\/01\/17\/new-jquery-plugin-savemonitor\/","title":{"rendered":"New jQuery plugin, <code>savemonitor<\/code>"},"content":{"rendered":"<p>Playing with the <a href=\"http:\/\/kavanot.me\/Home\/edit\">kavanot editor<\/a>, I wanted some way to encapsulate the process of noting that the text is \"dirty\"&mdash;meaning has been changed but not yet saved, and using a <a href=\"http:\/\/www.html5rocks.com\/en\/tutorials\/es6\/promises\/\">Promise<\/a> to express the idea that the text is soon-to-be-saved, then saved.<\/p>\r\n<p>I created a plugin that simply represents a state machine with four states, <code>clean<\/code>, <code>dirty<\/code>, <code>pending<\/code>, and <code>failed<\/code>. So you can do<\/p>\r\n<pre><code class=\"language-javascript\" >var monitor = $.savemonitor();\r\n$('textarea').on('input', function() { monitor.dirty() }); \/\/ track when the text area changes\r\n\/\/ call this function to save (say, as a click handler for a Save button\r\nfunction save(){\r\n  var text = $('textarea').val();\r\n  monitor.clean($.post('saveit.php', {data: text})); \/\/ jQuery Deferred's like $.post can be cast to Promises\r\n}\r\n\/\/ and keep track of the status\r\nmonitor.on('clean', function() {console.log('saved')})\r\nmonitor.on('failed', function() {console.log('not saved')})<\/code><\/pre>\r\n\r\n<p><a href=\"https:\/\/github.com\/dwachss\/bililiteRange\/blob\/master\/jquery.savemonitor.js\">See the code on github<\/a>.<\/p>\r\n<p>See a <a href=\"\/blog\/blogfiles\/vi\/savemonitor.html\">simple demo<\/a>.<\/p>\r\n<p>See a <a href=\"\/blog\/blogfiles\/vi\/savemonitor2.html\">demo<\/a> that uses <a href=\"http:\/\/fontawesome.io\/\">FontAwesome<\/a> icons and randomly fails to simulate a network failure.<\/p>\r\n<!--more-->\r\n<h3><code>$.savemonitor<\/code><\/h3>\r\n<pre><code class=\"language-javascript\" >var monitor = $.savemonitor([indicator]);<\/code><\/pre>\r\n<p>creates a monitor object with the following methods:<\/p>\r\n<dl>\r\n<dt><code class=\"language-javascript\" >setter(newstate {String})<\/code><\/dt>\r\n<dd>Returns a function that sets the state to <code class=\"language-javascript\" >newstate<\/code> and triggers an event with the same name. Uses some basic logic for the state transitions:\r\n<ol>\r\n<li>Doesn't trigger events if the state isn't changing<\/li>\r\n<li>Only allows the four states listed above<\/li>\r\n<li>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).<\/li>\r\n<li>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.<\/li>\r\n<\/ol>\r\n<\/dd>\r\n<dd>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 <code class=\"language-javascript\" >$('textarea').on('input', monitor.setter('dirty'));<\/code>.<\/dd>\r\n\r\n<dt><code class=\"language-javascript\" >dirty()<\/code><\/dt>\r\n<dd>Calls <code class=\"language-javascript\" >setter('dirty')()<\/code>, so it changes the state and triggers the event.<\/dd>\r\n\r\n<dt><code class=\"language-javascript\" >clean([resolver])<\/code><\/dt>\r\n<dd>Calls <code class=\"language-javascript\" >setter('pending')()<\/code>, then \r\n<code class=\"language-javascript\" >Promise.cast(resolver).then(this.setter('clean'), this.setter('failed'))<\/code>,\r\nso it takes advantage of the <a href=\"http:\/\/www.html5rocks.com\/en\/tutorials\/es6\/promises\/#toc-api\"><code class=\"language-javascript\" >Promise.cast<\/code><\/a> method to handle error handling. If <code class=\"language-javascript\" >resolver<\/code> is <code class=\"language-javascript\" >undefined<\/code> or anything without a <code class=\"language-javascript\" >then()<\/code> method, it immediately resolves and calls <code class=\"language-javascript\" >setter('clean')()<\/code>. If <code class=\"language-javascript\" >resolver<\/code> is what the Promises API calls a \"thenable\" (like a real Promise, or a jQuery Deferred) then waits for <em>that<\/em> to resolve and calls <code class=\"language-javascript\" >setter('clean')()<\/code> or <code class=\"language-javascript\" >setter('failed')()<\/code> as appropriate.<\/dd>\r\n\r\n<dt><code class=\"language-javascript\" >on(event {String}, handler {function})<\/code><\/dt>\r\n<dd>Sets up an event handler for the given event (will be one of <code>clean<\/code>, <code>dirty<\/code>, <code>pending<\/code>, and <code>failed<\/code>). Just uses <a href=\"http:\/\/api.jquery.com\/on\/\"><code class=\"language-javascript\" >$(this).on(event, handler)<\/code><\/a>, so you can use multiple event names, event namespaces, etc.<\/dd>\r\n\r\n<dt><code class=\"language-javascript\" >off(event {String}, handler {function})<\/code><\/dt>\r\n<dd>Removes the event handler for the given event. Uses <a href=\"http:\/\/api.jquery.com\/off\/\"><code class=\"language-javascript\" >$(this).off(event, handler)<\/code><\/a>.<\/dd>\r\n\r\n<dt><code class=\"language-javascript\" >trigger()<\/code><\/dt>\r\n<dd>Triggers the event for the current state.<\/dd>\r\n\r\n<dt><code class=\"language-javascript\" >state()<\/code><\/dt>\r\n<dd>Returns the current state.<\/dd>\r\n<\/dl>\r\n\r\n<p>If the <code class=\"language-javascript\" >indicator<\/code> parameter to <code class=\"language-javascript\" >$.savemonitor(indicator)<\/code> is set, then <code class=\"language-javascript\" >$(indicator)<\/code> is used as a target for the event changes, changing its class to the current state, with<\/p>\r\n<pre><code class=\"language-javascript\" >monitor.on('clean dirty failed pending', function (event){\r\n  $(indicator).removeClass('clean dirty failed pending');\r\n  $(indicator).addClass(event.type);\r\n});<\/code><\/pre>\r\n<p>so you can just use CSS to display the state, with no code at all.<\/p>\r\n\r\n<h3><code>$.fn.savemonitor<\/code><\/h3>\r\n<p>There's also a way to attach a monitor to an element directly, with <code class=\"language-javascript\" >var monitor = $(element).savemonitor(indicator)<\/code>. This stores the monitor object in <code class=\"language-javascript\" >$(element).data('savemonitor')<\/code> so it won't be created more than once. It returns the monitor object, so this plugin can't be chained.\r\nIt adds an event handler for the input event to track changes exactly as above, with <code class=\"language-javascript\" >$(element).on('input', monitor.setter('dirty')<\/code> so no other code is necessary.<\/p>\r\n\r\n<p>The simplest use is just two lines of code:<\/p>\r\n<pre><code class=\"language-html\" >\r\n&lt;style&gt;\r\n  button::after {\r\n    content: ' (' attr(class) ')'; \/* to show the state *\/\r\n  }\r\n&lt;\/style&gt;\r\n&lt;button&gt;Save&lt;\/button&gt;\r\n&lt;textarea&gt;&lt;\/textarea&gt;\r\n&lt;script&gt;\r\nvar monitor = $('textarea').savemonitor('button');\r\n$('button').click(function() {monitor.clean( $.post('saveToServer.php', {data: $('textarea').val()})) });\r\n&lt;\/script&gt;\r\n<\/code><\/pre>\r\n","protected":false},"excerpt":{"rendered":"Playing with the kavanot editor, I wanted some way to encapsulate the process of noting that the text is \"dirty\"&mdash;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, [&hellip;]","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,5],"tags":[],"_links":{"self":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/3164"}],"collection":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/comments?post=3164"}],"version-history":[{"count":10,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/3164\/revisions"}],"predecessor-version":[{"id":3180,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/3164\/revisions\/3180"}],"wp:attachment":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/media?parent=3164"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/categories?post=3164"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/tags?post=3164"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}