From b488e15947ae8c90c402e235fa78873b1a8ffeff Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 23 Jul 2013 11:00:32 -0700 Subject: [PATCH] CRM-12923, CRM-12943 - Track whether model changes have been saved to the server ---------------------------------------- * 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 | 72 ++++++++++++++++++++++++++++++++ tests/qunit/crm-backbone/test.js | 46 ++++++++++++++------ 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/js/crm.backbone.js b/js/crm.backbone.js index 2e2e381fe8..fbd52f6706 100644 --- a/js/crm.backbone.js +++ b/js/crm.backbone.js @@ -112,6 +112,78 @@ }); }; + /** + * 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 * diff --git a/tests/qunit/crm-backbone/test.js b/tests/qunit/crm-backbone/test.js index f4556a4d14..b9c47917b4 100644 --- a/tests/qunit/crm-backbone/test.js +++ b/tests/qunit/crm-backbone/test.js @@ -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(); + }); } }); }); -- 2.25.1