-
-
Save slindberg/8660986 to your computer and use it in GitHub Desktop.
/** | |
Ember Data: Dependent Relationships | |
This package extends Ember Data to support creating relationships | |
where a model's dirty state depends not only on its own attributes | |
but on the dirty state of models in dependent relationships as well. | |
```javascript | |
App.Thing = DS.Model.extend({ | |
name : DS.attr('string'), | |
children : DS.hasMany('thing', { dependent: true }) | |
}); | |
// Load all the things | |
var thing = store.findById('thing', '1'); | |
var child = thing.get('children.firstObject'); | |
thing.get('isDirty'); // false | |
child.get('name'); // 'foo' | |
child.set('name', 'bar'); | |
thing.get('isDirty'); // true | |
thing.rollback(); | |
child.get('name'); // 'foo' | |
``` | |
Note that saving dependent relations automatically, and handling | |
'isValid' state based on dependent relations is not supported. | |
*/ | |
/* global Ember, DS */ | |
(function() { | |
var get = Ember.get; | |
var set = Ember.set; | |
function isDescriptor(value) { | |
// Ember < 1.11 | |
if (Ember.Descriptor !== undefined) { | |
return value instanceof Ember.Descriptor; | |
} | |
// Ember >= 1.11 | |
return value && typeof value === 'object' && value.isDescriptor; | |
} | |
// | |
// State machine handlers | |
// | |
// Object/array agnostic 'isDirty' check | |
function isRelatedRecordDirty(value) { | |
return Ember.isArray(value) ? Ember.A(value).isAny('isDirty') : get(value, 'isDirty'); | |
} | |
// Original-state aware dirty check | |
function isRelationshipDirty(record, key) { | |
var value = get(record, key).toArray(); | |
var originalValue = record._dependentRelationships[key]; | |
return Ember.compare(value, originalValue) !== 0; | |
} | |
// The new de facto check to determine if a record is dirty | |
function isRecordDirty(record) { | |
// First check normal attributes | |
if (Ember.keys(record._attributes).length) { | |
return true; | |
} | |
// Then check dependent relations | |
return Ember.A(Ember.keys(record._dependentRelationships)).any(function(key) { | |
return isRelationshipDirty(record, key) || isRelatedRecordDirty(get(record, key)); | |
}); | |
} | |
// A dependent relationship can change if: | |
// * a belongsTo gets changed to another record | |
// * a belongsTo record dirties/cleans | |
// * a hasMany array gets added to or removed from | |
// * a hasMany array has a record that dirties/cleans | |
var dependentRelationshipDidChange = function(record, context) { | |
if (Ember.compare(context.value, context.originalValue) !== 0 || isRelatedRecordDirty(context.value)) { | |
record.send('becomeDirty'); | |
} else { | |
record.send('propertyWasReset', context.name); | |
} | |
}; | |
// The check for whether the record is still dirty now has to account for dependent relations | |
var propertyWasReset = function(record) { | |
if (!isRecordDirty(record)) { | |
record.send('rolledBack'); | |
} | |
}; | |
// Check to see if the saved record is dirty | |
var savedSetup = function(record) { | |
if (isRecordDirty(record)) { | |
record.adapterDidDirty(); | |
} | |
}; | |
// | |
// Perform some state machine surgery | |
// TODO: figure out how to make this less ass | |
// | |
// Handle dependent relationship change | |
DS.RootState.loaded.dependentRelationshipDidChange = dependentRelationshipDidChange; | |
// Changes to dependent relations while in-flight, invalid, or deleted should not alter its state | |
DS.RootState.loaded.created.inFlight.dependentRelationshipDidChange = Ember.K; | |
DS.RootState.loaded.updated.inFlight.dependentRelationshipDidChange = Ember.K; | |
DS.RootState.loaded.created.invalid.dependentRelationshipDidChange = Ember.K; | |
DS.RootState.loaded.updated.invalid.dependentRelationshipDidChange = Ember.K; | |
DS.RootState.deleted.dependentRelationshipDidChange = Ember.K; | |
// Override the property reset handler to account for dependent relations | |
DS.RootState.loaded.created.uncommitted.propertyWasReset = propertyWasReset; | |
DS.RootState.loaded.updated.uncommitted.propertyWasReset = propertyWasReset; | |
// Handle the case when a record that is in the 'root.deleted.uncommitted' state | |
// is rolled back but has dirty dependent relations | |
DS.RootState.loaded.saved.setup = savedSetup; | |
// | |
// Modify DS.Model | |
// | |
// Add dependent property helpers | |
DS.Model.reopenClass({ | |
// Loop over each dependent relation, passing the property name and the relationship meta | |
eachDependentRelationship: function(callback, binding) { | |
get(this, 'relationshipsByName').forEach(function(relationship, name) { | |
if (relationship.options.dependent) { | |
callback.call(binding, name, relationship); | |
} | |
}); | |
} | |
}); | |
DS.Model.reopen(Ember.Comparable, { | |
// Initialize dependent relationship snapshot object | |
_setup: function() { | |
this._super(); | |
this._dependentRelationships = {}; | |
}, | |
// Loop over each dependent property | |
eachDependentRelationship: function(callback, binding) { | |
this.constructor.eachDependentRelationship(callback, binding || this); | |
}, | |
// Hook into the object creation lifecycle in order to add dirty observers | |
didDefineProperty: function(proto, key, value) { | |
this._super(proto, key, value); | |
if (isDescriptor(value)) { | |
var meta = value.meta(); | |
if (meta.isRelationship && meta.options.dependent) { | |
if (meta.kind === 'belongsTo') { | |
Ember.addObserver(proto, key + '.isDirty', null, 'dependentRelationshipDidChange'); | |
} else if (meta.kind === 'hasMany') { | |
Ember.addObserver(proto, key + '[email protected]', null, 'dependentRelationshipDidChange'); | |
} | |
} | |
} | |
}, | |
// Returns object describing of changed relationships, like `changedAttributes` | |
changedRelationships: function() { | |
var record = this; | |
var dependentRelations = record._dependentRelationships; | |
var relationship; | |
var changed = {}; | |
record.eachDependentRelationship(function(name, relationshipMeta) { | |
if (record._relationships[name] && isRelationshipDirty(record, name)) { | |
relationship = get(record, name); | |
changed[name] = [ | |
Ember.copy(dependentRelations[name]), | |
relationshipMeta.kind === 'belongsTo' ? relationship : relationship.toArray(), | |
]; | |
} | |
}); | |
return changed; | |
}, | |
// Observer for relationship change, should send state machine message 'dependentRelationshipDidChange' | |
dependentRelationshipDidChange: Ember.immediateObserver(function(record, key) { | |
var dependentRelations = record._dependentRelationships; | |
var name = key.split('.')[0]; | |
if (name in dependentRelations) { | |
var value = get(record, name); | |
// Make DS.ManyArray into a vanilla array for comparison with original | |
if (Ember.isArray(value)) { | |
value = value.toArray(); | |
} | |
record.send('dependentRelationshipDidChange', { | |
name : name, | |
value : value, | |
originalValue : dependentRelations[name], | |
}); | |
} | |
}), | |
// Update the dependent relations when the adapter loads new data | |
adapterDidCommit: function() { | |
this.snapshotDependentRelations(); | |
this._super.apply(this, arguments); | |
// Relationship updates don't trigger data changes anymore, so manually | |
// notify all relationship properties of possible change | |
this.eachDependentRelationship(function(name, relationship) { | |
if (relationship.kind === 'hasMany') { | |
this.dependentRelationshipDidChange(this, name); | |
} | |
}); | |
}, | |
// When the record is loaded/saved, save its relations so they can be reverted | |
snapshotDependentRelations: function() { | |
var record = this; | |
var dependentRelations = record._dependentRelationships; | |
var relationship; | |
record.eachDependentRelationship(function(name, relationshipMeta) { | |
if (record._relationships[name]) { | |
relationship = get(record, name); | |
dependentRelations[name] = relationshipMeta.kind === 'belongsTo' ? relationship : relationship.toArray(); | |
} | |
}); | |
}.on('didLoad'), | |
// Dependent relations rely on the 'isDirty' CP, which may not get called | |
precomputeIsDirty: function() { | |
get(this, 'isDirty'); | |
}.on('init'), | |
// Rollback relations as well as attributes | |
rollback: function() { | |
// Revert attributes like normal | |
this._super(); | |
var record = this; | |
var dependentRelations = this._dependentRelationships; | |
record.eachDependentRelationship(function(name, relationshipMeta) { | |
if (name in dependentRelations) { | |
var originalRelationship = dependentRelations[name]; | |
if (relationshipMeta.kind === 'belongsTo') { | |
set(record, name, originalRelationship); | |
} else { | |
get(record, name).setObjects(originalRelationship); | |
} | |
// Rollback child/field records that have changed as well | |
Ember.makeArray(originalRelationship).filterBy('isDirty').invoke('rollback'); | |
} | |
}); | |
}, | |
// Basic identity comparison to allow `Ember.compare` to work on models | |
compare: function(r1, r2) { | |
return r1 === r2 ? 0 : 1; | |
}, | |
}); | |
}()); |
Thanks @kellyselden, updated.
This isn't working in ember-data 1.0.0-beta.19.1. It breaks on https://gist.github.com/slindberg/8660986#file-ember-data-dependent-relations-js-L71 where record._dependentRelationships
is undefined. record
was previously a DS.Model
instance, but now it is a DS.InternalModel
, which is a pojo that cannot be reopened.
Applied a fix for the toArray()
error on belongsTo
associations here: https://gist.github.com/pilaf/795409f42468546728a0
@kellyselden is right. Anyone has a fix?
@Leooo @kellyselden I need a fix for this as well. Did you guys make any progress?
Here is an attempt at internal model compatibility for dependent relations that is based on lytics/ember-data.model-fragments's compatibility work.
Ember 1.13.7
Data 1.13.14
got error here
if (value && typeof value === 'object' && value.isDescriptor) {
var meta = value.meta(); <-- meta is not a function
I'm not a familiar with ember data, so my solution is quite simple
if (value && typeof value === 'object' && value.isDescriptor && typeof value.meta === 'function')
This fork works https://gist.github.com/kellyselden/c5bcda57c69055b85711