Skip to content

Commit

Permalink
[FEATURE ds-reset-attribute] Add rolling back of a single model attri…
Browse files Browse the repository at this point in the history
…bute
  • Loading branch information
courajs committed May 17, 2016
1 parent 52ea222 commit 38545cf
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 2 deletions.
21 changes: 21 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,24 @@ entry in `config/features.json`.
* [404] `DS.NotFoundError`
* [409] `DS.ConflictError`
* [500] `DS.ServerError`

- `ds-reset-attribute` [#4246](https://github.com/emberjs/data/pull/4246)

Adds a `resetAttribute` method to models. Similar to `rollbackAttributes`,
but for only a single attribute.

```js
// { firstName: 'Tom', lastName: 'Dale' }
let tom = store.peekRecord('person', 1);
tom.setProperties({
firstName: 'Yehuda',
lastName: 'Katz'
});
tom.resetAttribute('firstName') // { firstName: 'Tom', lastName: 'Katz' }
tom.get('hasDirtyAttributes') // true
tom.resetAttribute('lastName') // { firstName: 'Tom', lastName: 'Dale' }
tom.get('hasDirtyAttributes') // false
```
21 changes: 20 additions & 1 deletion addon/-private/system/model/internal-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import RootState from "ember-data/-private/system/model/states";
import Relationships from "ember-data/-private/system/relationships/state/create";
import Snapshot from "ember-data/-private/system/snapshot";
import EmptyObject from "ember-data/-private/system/empty-object";
import isEnabled from 'ember-data/-private/features';

import {
getOwner
Expand Down Expand Up @@ -434,6 +435,7 @@ InternalModel.prototype = {
this.record._notifyProperties(dirtyKeys);

},

/*
@method transitionTo
@private
Expand Down Expand Up @@ -849,4 +851,21 @@ InternalModel.prototype = {

return reference;
}
};
}

if (isEnabled('ds-reset-attribute')) {
/*
Returns the latest truth for an attribute - the canonical value, or the
in-flight value.
@method latestTrueValue
@private
*/
InternalModel.prototype.latestTrueValue = function latestTrueValue(key) {
if (key in this._inFlightAttributes) {
return this._inFlightAttributes[key];
} else {
return this._data[key];
}
};
}
27 changes: 27 additions & 0 deletions addon/-private/system/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BelongsToMixin } from 'ember-data/-private/system/relationships/belongs
import { HasManyMixin } from 'ember-data/-private/system/relationships/has-many';
import { DidDefinePropertyMixin, RelationshipsClassMethodsMixin, RelationshipsInstanceMethodsMixin } from 'ember-data/-private/system/relationships/ext';
import { AttrClassMethodsMixin, AttrInstanceMethodsMixin } from 'ember-data/-private/system/model/attr';
import isEnabled from 'ember-data/-private/features';

/**
@module ember-data
Expand Down Expand Up @@ -982,6 +983,32 @@ if (Ember.setOwner) {
});
}

if (isEnabled('ds-reset-attribute')) {
Model.reopen({
/**
Discards any unsaved changes to the given attribute.
Example
```javascript
record.get('name'); // 'Untitled Document'
record.set('name', 'Doc 1');
record.get('name'); // 'Doc 1'
record.resetAttribute('name');
record.get('name'); // 'Untitled Document'
```
@method resetAttribute
*/
resetAttribute(attributeName) {
if (attributeName in this._internalModel._attributes) {
this.set(attributeName, this._internalModel.latestTrueValue(attributeName));
}
}
});
}


Model.reopenClass(RelationshipsClassMethodsMixin);
Model.reopenClass(AttrClassMethodsMixin);

Expand Down
3 changes: 2 additions & 1 deletion config/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"ds-pushpayload-return": null,
"ds-serialize-ids-and-types": true,
"ds-extended-errors": null,
"ds-links-in-record-array": null
"ds-links-in-record-array": null,
"ds-reset-attribute": null
}
178 changes: 178 additions & 0 deletions tests/unit/model-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,184 @@ test("changedAttributes() works while the record is being updated", function(ass
});
});

if (isEnabled('ds-reset-attribute')) {
test("resetAttribute() reverts a single attribute to its canonical value", function(assert) {
assert.expect(5);

run(function() {
store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Peter',
isDrugAddict: true
}
}
});

let person = store.peekRecord('person', 1);

assert.equal(person.get('hasDirtyAttributes'), false, "precond - person record should not be dirty");
person.setProperties({
name: 'Piper',
isDrugAddict: false
});
assert.equal(person.get('hasDirtyAttributes'), true, "record becomes dirty after setting property to a new value");
person.resetAttribute('isDrugAddict');
assert.equal(person.get('isDrugAddict'), true, "The specified attribute is rolled back");
assert.equal(person.get('name'), 'Piper', "Unspecified attributes are not rolled back");
assert.equal(person.get('hasDirtyAttributes'), true, "record with changed attributes is still dirty");
});
});

test("calling resetAttribute() on an unmodified property has no effect", function(assert) {
assert.expect(5);

run(function() {
store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Peter',
isDrugAddict: true
}
}
});

let person = store.peekRecord('person', 1);

assert.equal(person.get('hasDirtyAttributes'), false, "precond - person record should not be dirty");
person.set('name', 'Piper');
assert.equal(person.get('hasDirtyAttributes'), true, "record becomes dirty after setting property to a new value");
person.resetAttribute('isDrugAddict');
assert.equal(person.get('isDrugAddict'), true, "The specified attribute does not change value");
assert.equal(person.get('name'), 'Piper', "Unspecified attributes are not rolled back");
assert.equal(person.get('hasDirtyAttributes'), true, "record with changed attributes is still dirty");
});
});

test("Rolling back the final value with resetAttribute() causes the record to become clean again", function(assert) {
assert.expect(3);

run(function() {
store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Peter',
isDrugAddict: true
}
}
});

let person = store.peekRecord('person', 1);

assert.equal(person.get('hasDirtyAttributes'), false, "precond - person record should not be dirty");
person.set('isDrugAddict', false);
assert.equal(person.get('hasDirtyAttributes'), true, "record becomes dirty after setting property to a new value");
person.resetAttribute('isDrugAddict');
assert.equal(person.get('hasDirtyAttributes'), false, "record becomes clean after resetting property to the old value");
});
});

test("Using resetAttribute on an in-flight record reverts to the latest in-flight value", function(assert) {
assert.expect(4);

var person, finishSaving;

// Make sure the save is async
env.adapter.updateRecord = function(store, type, snapshot) {
return new Ember.RSVP.Promise(function(resolve, reject) {
finishSaving = resolve;
});
};

run(function() {
store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: "Tom"
}
}
});
person = store.peekRecord('person', 1);
person.set('name', "Thomas");

person.save();
});

run(function() {
assert.equal(person.get('isSaving'), true);
assert.equal(person.get('name'), "Thomas");

person.set('name', "Tomathy");
assert.equal(person.get('name'), "Tomathy");

person.resetAttribute('name');
assert.equal(person.get('name'), "Thomas");

finishSaving();
});
});

test("Saving an in-flight record updates the in-flight value resetAttribute will use", function(assert) {
assert.expect(7);

var person, finishSaving;

// Make sure the save is async
env.adapter.updateRecord = function(store, type, snapshot) {
return new Ember.RSVP.Promise(function(resolve, reject) {
finishSaving = resolve;
});
};

run(function() {
store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: "Tom"
}
}
});
person = store.peekRecord('person', 1);
person.set('name', "Thomas");

person.save();
});

run(function() {
assert.equal(person.get('isSaving'), true);
assert.equal(person.get('name'), "Thomas");

person.set('name', "Tomathy");
assert.equal(person.get('name'), "Tomathy");

person.save()
});

run(function() {
assert.equal(person.get('isSaving'), true);
assert.equal(person.get('name'), "Tomathy");

person.set('name', "Tomny");
assert.equal(person.get('name'), "Tomny");

person.resetAttribute('name');
assert.equal(person.get('name'), 'Tomathy');

finishSaving();
});
});
}

test("a DS.Model does not require an attribute type", function(assert) {
var Tag = DS.Model.extend({
name: DS.attr()
Expand Down

0 comments on commit 38545cf

Please sign in to comment.