});
};
+ /**
+ * 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
*
var ContactModel = Backbone.Model.extend({});
CRM.Backbone.extendModel(ContactModel, 'Contact');
+CRM.Backbone.trackSaved(ContactModel);
var ContactCollection = Backbone.Collection.extend({
model: ContactModel
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) {
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();
+ }
+ });
});
}
});
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();
+ });
}
});
});