CRM-12923, CRM-12943 - Track whether model changes have been saved to the server
authorTim Otten <totten@civicrm.org>
Tue, 23 Jul 2013 18:00:32 +0000 (11:00 -0700)
committerTim Otten <totten@civicrm.org>
Fri, 26 Jul 2013 05:24:31 +0000 (22:24 -0700)
----------------------------------------
* CRM-12923: Create HTML prototype of "Job Position" UI
  http://issues.civicrm.org/jira/browse/CRM-12923
* CRM-12943: Make HTML prototype of job UI functional
  http://issues.civicrm.org/jira/browse/CRM-12943

js/crm.backbone.js
tests/qunit/crm-backbone/test.js

index 2e2e381fe82804720dde8c7b939c8673fb91edaf..fbd52f6706a87e30b203087155489829e7e229f0 100644 (file)
     });
   };
 
+  /**
+   * Configure a model class to track whether a model has unsaved changes.
+   *
+   * The ModelClass will be extended with:
+   *  - Method: isSaved() - true if there have been no changes to the data since the last fetch or save
+   *  - Event: saved(object model, bool is_saved) - triggered whenever isSaved() value would change
+   *
+   *  Note: You should not directly call isSaved() within the context of the success/error/sync callback;
+   *  I haven't found a way to make isSaved() behave correctly within these callbacks without patching
+   *  Backbone. Instead, attach an event listener to the 'saved' event.
+   *
+   * @param ModelClass
+   */
+  CRM.Backbone.trackSaved = function(ModelClass) {
+    // Retain references to some of the original class's functions
+    var Parent = _.pick(ModelClass.prototype, 'initialize', 'save', 'fetch');
+
+    // Private callback
+    var onSyncSuccess = function() {
+      this._modified = false;
+      if (this._oldModified.length > 0) {
+        this._oldModified.pop();
+      }
+      this.trigger('saved', this, this.isSaved());
+    };
+    var onSaveError = function() {
+      if (this._oldModified.length > 0) {
+        this._modified = this._oldModified.pop();
+        this.trigger('saved', this, this.isSaved());
+      }
+    };
+
+    // Defaults - if specified in ModelClass, preserve
+    _.defaults(ModelClass.prototype, {
+      isSaved: function() {
+        var result = !this.isNew() && !this._modified;
+        return result;
+      },
+      _saved_onchange: function(model, options) {
+        if (options.parse) return;
+        var oldModified = this._modified;
+        this._modified = true;
+        if (!oldModified) {
+          this.trigger('saved', this, this.isSaved());
+        }
+      }
+    });
+
+    // Overrides - if specified in ModelClass, replace
+    _.extend(ModelClass.prototype, {
+      initialize: function(options) {
+        this._modified = false;
+        this._oldModified = [];
+        this.listenTo(this, 'change', this._saved_onchange);
+        this.listenTo(this, 'error', onSaveError);
+        this.listenTo(this, 'sync', onSyncSuccess);
+        if (Parent.initialize) {
+          return Parent.initialize.apply(this, arguments);
+        }
+      },
+      save: function() {
+        // we'll assume success
+        this._oldModified.push(this._modified);
+        return Parent.save.apply(this, arguments);
+      },
+      fetch: function() {
+        this._oldModified.push(this._modified);
+        return Parent.fetch.apply(this, arguments);
+      }
+    });
+  };
+
   /**
    * Connect a "collection" class to CiviCRM's APIv3
    *
index f4556a4d146cc64baec9475b44c8dd1135d4881b..b9c47917b410972a919b0536109866554bbe4e6a 100644 (file)
@@ -5,6 +5,7 @@ var MALFORMED_CONTACT_ID = 'z';
 
 var ContactModel = Backbone.Model.extend({});
 CRM.Backbone.extendModel(ContactModel, 'Contact');
+CRM.Backbone.trackSaved(ContactModel);
 
 var ContactCollection = Backbone.Collection.extend({
   model: ContactModel
@@ -80,27 +81,33 @@ asyncTest("create/read/delete/read (ok)", function() {
     first_name: "George" + TOKEN,
     last_name: "Anon" + TOKEN
   });
+  equal(c1.isSaved(), false, "");
 
   // Create the new contact
   c1.save({}, {
     error: onUnexpectedError,
     success: function() {
       equal(c1.get("first_name"), "George" + TOKEN, "save() should return new first name");
+      equal(c1.isSaved(), true, "");
 
       // Fetch the newly created contact
       var c2 = new ContactModel({id: c1.get('id')});
+      equal(c2.isSaved(), true, "");
       c2.fetch({
         error: onUnexpectedError,
         success: function() {
           equal(c2.get("first_name"), c1.get("first_name"), "fetch() should return first name");
+          equal(c2.isSaved(), true, "");
 
           // Destroy the newly created contact
           c2.destroy({
             error: onUnexpectedError,
             success: function() {
+              equal(c2.isSaved(), true, "");
 
               // Attempt (but fail) to fetch the deleted contact
               var c3 = new ContactModel({id: c1.get('id')});
+              equal(c3.isSaved(), true, "");
               c3.fetch({
                 success: onUnexpectedSuccess,
                 error: function(model, error) {
@@ -139,20 +146,27 @@ module('model - update');
 asyncTest("update (ok)", function() {
   var NICKNAME = "George" + new Date().getTime();
   var c = new ContactModel({id: VALID_CONTACT_ID});
-  c.save({
+  equal(c.isSaved(), true, "");
+  c.set({
     nick_name: NICKNAME
-  }, {
+  });
+  equal(c.isSaved(), false, "");
+  c.save({}, {
     error: onUnexpectedError,
     success: function() {
       equal(c.get("nick_name"), NICKNAME, "save() should return new nickname");
-
-      var c2 = new ContactModel({id: VALID_CONTACT_ID});
-      c2.fetch({
-        error: onUnexpectedError,
-        success: function() {
-          equal(c2.get("nick_name"), NICKNAME, "fetch() should return new nickname");
-          start();
-        }
+      _.defer(function(){
+        equal(c.isSaved(), true, "");
+
+        // read back - make sure the save worked
+        var c2 = new ContactModel({id: VALID_CONTACT_ID});
+        c2.fetch({
+          error: onUnexpectedError,
+          success: function() {
+            equal(c2.get("nick_name"), NICKNAME, "fetch() should return new nickname");
+            start();
+          }
+        });
       });
     }
   });
@@ -161,13 +175,19 @@ asyncTest("update (ok)", function() {
 asyncTest("update (error)", function() {
   var NICKNAME = "George" + new Date().getTime();
   var c = new ContactModel({id: VALID_CONTACT_ID});
-  c.save({
+  equal(c.isSaved(), true, "");
+  c.set({
     contact_type: 'Not-a.va+lidConta(ype'
-  }, {
+  });
+  equal(c.isSaved(), false, "");
+  c.save({}, {
     success: onUnexpectedSuccess,
     error: function(model, error) {
       assertApiError(error);
-      start();
+      _.defer(function(){
+        equal(c.isSaved(), false, "");
+        start();
+      });
     }
   });
 });