Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dirty checking and only PATCHing modified fields #9

Merged
merged 5 commits into from Mar 6, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions src/angular-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ var noop = angular.noop,
isFunc = angular.isFunction,
isObject = angular.isObject,
isArray = angular.isArray,
isUndef = angular.isUndefined;
isUndef = angular.isUndefined,
equals = angular.equals;

function deepExtend(dst, source) {
for (var prop in source) {
Expand Down Expand Up @@ -65,7 +66,9 @@ angular.module('ur.model', []).provider('model', function() {
return model.collection(data, true);
}
if (data && JSON.stringify(data).length > 3) {
return extend(object, data);
var updated = extend(object, data);
copy(data, object.$original);
return updated;
}
return object;
}
Expand Down Expand Up @@ -122,24 +125,34 @@ angular.module('ur.model', []).provider('model', function() {
create: function(data) {
return this.instance(deepExtend(copy(this.$config().defaults), data || {}));
},

// @todo Get methods for related objects
$related: function(object) {
if (!object.$links) return [];

forEach(object.$links, function(url, name) {
object.prototype[name] = function() {

};
});
}
},

// Methods available on model instances
$instance: {
$original: {},
$save: function(data) {
var method = this.$exists() ? 'PATCH' : 'POST';
return $request(this, this.$model(), method, deepExtend(this, data ? copy(data) : {}));
var method, requestData;

if (this.$exists()) {
method = 'PATCH';
requestData = data ? copy(data) : this.$modified();
} else {
method = 'POST';
requestData = deepExtend(this, data ? copy(data) : {});
}

return $request(this, this.$model(), method, requestData);
},
$delete: function() {
return $request(this, this.$model(), 'DELETE');
Expand All @@ -148,10 +161,29 @@ angular.module('ur.model', []).provider('model', function() {
return $request(this, this.$model(), 'GET');
},
$revert: function() {
// @todo Reset to previously loaded state
for (var prop in this.$original) {
this[prop] = copy(this.$original[prop]);
}
},
$exists: function() {
return !!expr(this, this.$model().$config().identity).get();
},
$dirty: function() {
return !this.$pristine();
},
$pristine: function() {
return equals(this, this.$original);
},
$modified: function() {
var diff = {};

for (var prop in this.$original) {
if (!equals(this[prop], this.$original[prop])) {
diff[prop] = this[prop];
}
}

return diff;
}
},

Expand Down Expand Up @@ -272,6 +304,7 @@ angular.module('ur.model', []).provider('model', function() {
},
instance: function(data) {
options.$instance = inherit(new ModelInstance(this), options.$instance);
options.$instance.$original = copy(data) || {};
return inherit(options.$instance, data || {});
},
collection: function(data, boxElements) {
Expand Down
186 changes: 185 additions & 1 deletion test/modelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe("model", function() {
archived: false
}, update = angular.extend({}, doc, { archived: true });

$httpBackend.expectPATCH(url, JSON.stringify(update)).respond(200, JSON.stringify(update));
$httpBackend.expectPATCH(url, JSON.stringify({ archived: true })).respond(200, JSON.stringify(update));

var existingProject = model("Projects").instance(angular.extend({ $links: { self: url } }, doc));

Expand Down Expand Up @@ -304,5 +304,189 @@ describe("model", function() {
$httpBackend.flush();
}));
});

describe('dirty checking', function() {
var user;

beforeEach(inject(function(model) {
user = model('Users').instance({
$links: {
self: "http://api/users/100"
},
_id: 100,
name: "Some Person",
role: "User",
telephone: {
home: '1234',
office: '5678'
}
});
}));

it('should tell if a value has changed', function() {
expect(user.$dirty()).toBe(false);
expect(user.$pristine()).toBe(true);

user.name = "Some other person";

expect(user.$dirty()).toBe(true);
expect(user.$pristine()).toBe(false);
});

it('should tell if a nested object has changed', function() {
expect(user.$dirty()).toBe(false);
expect(user.$pristine()).toBe(true);

user.telephone.office = "91011";

expect(user.$dirty()).toBe(true);
expect(user.$pristine()).toBe(false);
});

it('should revert to original state', function() {
user.name = "Some other person";
user.telephone.office = "91011";

user.$revert();

expect(user.$dirty()).toBe(false);

var reverted = angular.extend({}, user, {
_id: 100,
name: "Some Person",
role: "User",
telephone: {
home: "1234",
office: "5678"
}
});

expect(angular.equals(reverted, user)).toBe(true);
expect(user.$original.telephone).not.toBe(user.telephone);
});

it('should return modified fields', function() {
angular.extend(user, {
name: "New name",
role: "Admin",
telephone: {
office: "91011"
}
});

expect(user.$modified()).toEqual({
name: "New name",
role: "Admin",
telephone: {
office: "91011"
}
});
});

it('should only PATCH modified fields', function() {
angular.extend(user, {
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
});

user.$save();

$httpBackend.expectPATCH('http://api/users/100', {
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
}).respond(200);
$httpBackend.flush();
});

it('should update original state on successful save', function() {
angular.extend(user, {
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
});

user.$save();

$httpBackend.expectPATCH('http://api/users/100', {
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
}).respond(200, {
$links: {
self: "http://api/users/100"
},
_id: 100,
name: "Newman",
role: "Intern",
telephone: {
home: "0123",
office: "5678"
}
});
$httpBackend.flush();

expect(user.$pristine()).toBe(true);
expect(user.$dirty()).toBe(false);
expect(user.$modified()).toEqual({});

expect(user.$original.telephone).not.toBe(user.telephone);
});

it('should preserve modified state on failed save', function() {
angular.extend(user, {
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
});

user.$save();

$httpBackend.expectPATCH('http://api/users/100', {
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
}).respond(500);
$httpBackend.flush();

expect(user.$pristine()).toBe(false);
expect(user.$dirty()).toBe(true);
expect(user.$modified()).toEqual({
name: "Newman",
role: "Intern",
telephone: {
home: "0123"
}
});
});

it('should ignore dirty fields when saving a new instance', inject(function(model) {
var admin = model('Users').create({
username: "Mr Admin",
role: "Admin"
});

admin.$save();

$httpBackend.expectPOST('http://api/users', {
username: "Mr Admin",
role: "Admin"
}).respond(200);
$httpBackend.flush();
}));
});
});
});