Skip to content

Instantly share code, notes, and snippets.

@amkirwan
Last active December 18, 2020 18:44
Show Gist options
  • Save amkirwan/43faf1d85b227911f331affa9094ae37 to your computer and use it in GitHub Desktop.
Save amkirwan/43faf1d85b227911f331affa9094ae37 to your computer and use it in GitHub Desktop.
Ember.js rollback model and relationships
//mixins/rollback-relationships.js
import Ember from 'ember';
import DS from 'ember';
export default Ember.Mixin.create({
cacheRelationships: Ember.computed(function() { return Ember.A(); }),
tmpRecords: Ember.computed(function() { return Ember.A(); }),
dirtyRelationships: Ember.computed(function() { return Ember.A(); }),
originalRelationships: Ember.computed(function() { return Ember.Object.create() }),
hasDirtyRelationship: Ember.computed(function() { return false; }),
dirtyTracker: Ember.Object.create(),
// use it to check if the model has a dirty attribute or a dirty relationship
isExtraDirty: Ember.computed('dirtyTracker', function() {
if (this.get('hasDirtyAttributes')) { return true; }
let dirty = false;
let dirtyTracker = this.get('dirtyTracker');
let keys = Object.keys(dirtyTracker);
for (let i=0, len=keys.length; i < len; i++) {
if (dirtyTracker[keys[i]] === true) {
dirty = true;
break;
}
}
return dirty;
}).volatile(),
dirtyRelationshipTracking: Ember.on('init', function() {
let model = this;
model.get('cacheRelationships').forEach((key) => {
let rel = model.relationshipFor(key);
if (rel.kind === 'hasMany') {
model.addObserver(`${key}.@each.hasDirtyAttributes`, (sender, key, value, rev) => {
Ember.run.once(() => {
let attrRelationship = key.split(/\./)[0]
let dirtyModels = sender.get(attrRelationship).filterBy('hasDirtyAttributes', true);
if (Ember.isEmpty(dirtyModels)) {
this.get('dirtyTracker').set(attrRelationship, false);
this.set('hasDirtyRelationship', false);
this.get('dirtyRelationships').removeObject(attrRelationship);
} else {
this.get('dirtyTracker').set(attrRelationship, true);
this.set('hasDirtyRelationship', true);
this.get('dirtyRelationships').addObject(attrRelationship);
}
});
});
} else if (rel.kind === 'belongsTo') {
model.addObserver(`${key}`, (sender, key, value, rev) => {
Ember.run.once(() => {
let attrRelationship = key;
// do not check if the relationship is undefined
if (this.get(`originalRelationships.${key}`)) {
if (Ember.isEqual(this.get(`${key}.id`), this.get(`originalRelationships.${key}`))) {
this.get('dirtyTracker').set(attrRelationship, false);
this.set('hasDirtyRelationship', false);
this.get('dirtyRelationships').removeObject(key);
} else {
this.get('dirtyTracker').set(attrRelationship, true);
this.set('hasDirtyRelationship', true);
this.get('dirtyRelationships').addObject(key);
}
} else {
this.get('dirtyTracker').set(attrRelationship, false);
}
});
});
}
});
}),
relationshipsCacheAll() {
let model = this;
if (model.get('cacheRelationships').length > 0) {
model.get('cacheRelationships').forEach((key) => {
let rel = model.relationshipFor(key);
if (rel.kind === 'belongsTo') {
this._cacheBelongsTo(key);
}
if (rel.kind === 'hasMany') {
this._cacheHasMany(key);
}
});
}
},
relationshipsCache() {
this.clearTmpRecords();
this.cacheOriginalRelationships();
},
_cacheBelongsTo(key) {
let model = this;
let belongsRef = model.belongsTo(key);
if (belongsRef.belongsToRelationship.isAsync) {
model.get(key).then((m) => {
m.relationshipsCacheAll();
});
} else {
model.get(key).relationshipsCacheAll();
}
model.relationshipsCache();
},
_cacheHasMany(key) {
let model = this;
let manyRef = model.hasMany(key);
if (manyRef.hasManyRelationship.isAsync) {
model.get(key).then((m) => {
m.invoke('relationshipsCacheAll');
});
} else {
model.get(key).forEach((m) => {
m.invoke('relationshipsCacheAll');
});
}
model.relationshipsCache();
},
createRecord(name) {
let record = this.store.createRecord(name);
this.get('tmpRecords').pushObject(record);
return record;
},
destroyTmpRecords() {
this.get('tmpRecords').invoke('destroyRecord');
this.clearTmpRecords();
},
clearTmpRecords() {
this.get('tmpRecords').clear();
},
rollbackAll() {
let model = this;
if (model.get('isExtraDirty')) {
if (model.get('cacheRelationships').length === 0) {
model.rollbackModelAttributes();
model.destroyTmpRecords();
return;
} else {
model.get('cacheRelationships').forEach((key) => {
let rel = model.relationshipFor(key);
if (rel.kind === 'belongsTo') {
this._rollbackBelongsTo(key);
}
if (rel.kind === 'hasMany') {
this._rollbackHasMany(key);
}
});
}
}
},
_rollbackBelongsTo(key) {
let model = this;
let belongsRef = model.belongsTo(key);
if (belongsRef.belongsToRelationship.isAsync) {
model.get(key).then((m) => {
m.rollbackAll();
});
} else {
model.get(key).rollbackAll();
}
model.rollbackRelationships();
},
_rollbackHasMany(key) {
let model = this;
let manyRef = model.hasMany(key);
if (manyRef.hasManyRelationship.isAsync) {
model.get(key).then((m) => {
m.invoke('rollbackAll');
});
} else {
model.get(key).forEach((m) => {
m.invoke('rollbackAll');
});
}
model.rollbackRelationships();
},
rollbackRelationships() {
let model = this;
if (this.get('cacheRelationships').length === 0) {
return;
} else {
this.get('cacheRelationships').forEach((key) => {
this.rollbackModelAttributes();
this._rollbackOriginalRelationships(key);
});
}
},
cacheOriginalRelationships() {
let model = this;
if (this.get('cacheRelationships').length === 0) {
model.eachRelationship((key, relationship) => {
this._setOriginalRelationships(key);
});
} else {
this.get('cacheRelationships').forEach((key) => {
this._setOriginalRelationships(key);
});
}
},
rollbackModelAttributes() {
let model = this;
if (model.get('hasDirtyAttributes')) {
model.rollbackAttributes();
}
},
_setOriginalRelationships(key) {
let model = this;
let or = model.get('originalRelationships');
let rel = model.relationshipFor(key);
if (rel.kind === 'belongsTo') {
or.set(key, model.belongsTo(key).id());
}
if (rel.kind === 'hasMany') {
or.set(key, model.hasMany(key).ids());
}
},
_rollbackOriginalRelationships(key) {
let model = this;
let rel = model.relationshipFor(key);
if (rel.kind === 'belongsTo') {
let rel_id = model.get(`originalRelationships.${key}`);
if (!Ember.isEmpty(rel_id)) {
model.set(key, this.store.peekRecord(key, rel_id));
}
}
if (rel.kind === 'hasMany') {
let rel_ids = model.get(`originalRelationships.${key}`);
if (!Ember.isEmpty(rel_ids)) {
model.get(key).clear();
rel_ids.forEach((rel_id) => {
model.get(key).pushObject(this.store.peekRecord(Ember.String.singularize(key), rel_id));
});
}
}
}
});
// Then import rollback-relationships into your model
import attr from 'ember-data/attr';
import Model from 'ember-data/model';
import { hasMany } from 'ember-data/relationships';
import Rollback from 'app/mixins/rollback-relationships';
export default Model.extend(Rollback, {
username: attr('string'),
firstname: attr('string'),
middlename: attr('string'),
lastname: attr('string'),
phoneNumbers: hasMany('phoneNumber', { async: true }),
emailAddresses: hasMany('emailAddress', { async: true }),
cacheRelationships: ['phoneNumbers', 'emailAddresses'],
});
// Then in your route call relationshipsCacheAll() to cache the current relationships of the model
// And to rollback the relationships call rollbackAll()
// user/edit/route.js
import Ember from 'ember';
export default Ember.Route.extend({
afterModel(model, transition) {
model.relationshipsCacheAll();
}
actions: {
submit(mode) {
model.save();
},
cancelSave(model) {
model.rollbackAll();
}
}
}
@ecairol
Copy link

ecairol commented Sep 6, 2018

@amkirwan thanks for sharing this code. I'm getting a "Assertion Failed: '{}' does not appear to be an ember-data model" as soon as I import the Mixin into my Model. Any ideas what could be wrong? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment