Reputation: 53
I'm not able to use Ember Data because our data model is a bit complex (for instance, not everything has an id) and I didn't like the caching and some other things about it; however, I do like that it's models have state flags (isNew, isDeleting) so I'm trying to implement some of the functionality.
The basic gist is that whenever an model is retrieved from the server, that data is stored in the _data object. Whenever a user then changes a value of something, that's stored in the _attributes object. By comparing the two, I can see if the model is dirty. Attributes are defined with the app.attr method.
app.Person = app.Model.extend({
"id": app.attr("number"),
"name": app.attr("string"),
"age": app.attr("number")
});
The attr function is very similar to ember data's.
function getValue(record, key, options) {
var attrs = get(record, "_attributes"),
data = get(record, "_data");
if ( attrs.hasOwnProperty(key) ) {
return attrs[key];
} else if ( data.hasOwnProperty(key) ) {
return data[key];
} else if ( typeof options.defaultValue === "function" ) {
return options.defaultValue();
}
return options.defaultValue; // may be undefined
}
app.attr = function(type, options) {
options = options || {};
var meta = {
"type": type,
"isAttribute": true,
"options": options
};
return function(key, value) {
if ( arguments.length > 1 ) {
if ( key === "id" ) {
console.error("id is readonly on "+this.constructor.toString());
} else {
if ( get(this, "_data."+key) === value ) {
delete get(this, "_attributes")[key]; // not sure if this really works? seems to from my testing. set(this, "_attributes."+key, undefined) literally set undefined, which did not work
} else {
set(this, "_attributes."+key, value);
}
}
}
return getValue(this, key, options);
}.property("_data").meta(meta);
};
Then on my model, I have certain flags like isNew and isDirty. It made sense for isDirty to be a computed property, but it needs to update whenever any attribute changes. I thought perhaps .property("_attributes.*") would do this, but it does not. I'm stumped. I tried making it volatile.. which ensures I get the correct value when explicitly asking for it, but templates don't update when the underlying properties change.
For my model above, it would be appropriate to do .property("id", "name", "age") but those vary model to model. I'm able to get the current model's properties in the model's init but I can't find a way to then change them dynamically.
Here's my model:
app.Model = Ember.Object.extend({
"_attributes": null,
"_data": null,
"isDeleted": false,
"isNew": false,
"isSaving": false,
"isDirty": function() {
var attrs = get(this, "_attributes");
for ( k in attrs ) {
if ( attrs.hasOwnProperty(k) ) {
return true;
}
}
return false;
}.property("_attributes").readOnly(), // NOTE: not quite right
"init": function() {
this._super();
set(this, "_data", {});
set(this, "_attributes", {});
var attrs = [];
this.constructor.eachAttribute(function(name, meta) {
attrs.push(name);
});
// NOTE: Can I do anything to attach all the attrs to isDirty here?
}
});
app.Model.reopenClass({
"attributes": Ember.computed(function() {
var map = Ember.Map.create();
this.eachComputedProperty(function(name, meta) {
if ( meta.isAttribute ) {
meta.name = name;
map.set(name, meta);
}
});
return map;
}),
"eachAttribute": function(callback, binding) {
get(this, "attributes").forEach(function(name, meta) {
callback.apply(binding, arguments);
}, binding);
}
});
I created a jsFiddle with the code and my tests: http://jsfiddle.net/2uCn3/3/
So any idea how I can get isDirty to update whenever an attribute changes? Similary (as can be seen in the jsFiddle), the attr will still return the old default value if _data.someProperty is changed directly so I made a test for that too.. it's essentially the same issue though.
I considered having a counter like _attributesChanged and using incrementProperty but it didn't seem reliable and looks atrocious.
I also considered converting _attributes and _data into arrays and then perhaps the @each would be of some use.
Hoping there's a cleaner approach. I'm still trying to figure out how ember data does it.
UPDATE: I found that Ember Data also bugs if you manipulate a _data.property directly, I had come up with a workaround but GJK's solution is infinitely better.
Upvotes: 0
Views: 2606
Reputation: 37369
I wrote my own persistence layer for Ember as well, and the way I solved it was to call this.notifyPropertyChange('_attributes')
every time I changed the _attributes
object. I can't see all of your code, but if you restrict editing _attributes
to just a few mutator methods, then you should only need to call it in 3 or 4 places. Then for your computed property, you can just have it depend on the _attributes
property.
In your code, it might be enough to just call it in two place inside the attr
function. (Assuming that this is the only way to modify the contents of _attributes
.)
if ( get(this, "_data."+key) === value ) {
delete get(this, "_attributes")[key];
this.notifyPropertyChange('_attributes');
} else {
set(this, "_attributes."+key, value);
this.notifyPropertyChange('_attributes');
}
Upvotes: 4