Ember.Object
provides a flexible starting point for creating models in a single-page web application. It can hold simple data members, define computed properties that automatically update when dependencies change, run a callback when properties change, and extend parent “classes” to create new types with additional members.
That said, I think Ember can be a bit too flexible at times.
How Much Flexibility is Too Much?
Let’s say I create a model for a blog post using Ember Data:
export default DS.Model.extend({
author: DS.hasOne('author'),
text: DS.attr('string'),
likes: DS.attr('number'),
comments: DS.hasMany('comment')
});
Then in code later on, I can do the following:
const post = this.store.create('blog-post', {
author: jim, likes: 5, comments: []
});
post.set('text', "Lorem ipsum dolor sit amet...");
post.save();
That’s all well and good. But what if later on, I want to set the body of the post, forget I called it ‘text,’ and think I called it ‘body’?
post2.set('body', "Ain't goin' nowhere");
// a new property 'body' was created and has the set value
post.save();
My tests (automated and manual) will probably fail because the post content is not being updated properly, but it may not be immediately clear what the problem is. This issue is further exacerbated by many-word property names:
const data = this.get('reallyLongPropertyNameEtc1');
or when you are dot-chaining in property gets or sets:
const data = this.get('foo.bar.baz');
If data
is undefined, which part is to blame: foo
or bar
or baz
? Again, we can dig in and figure out the answer, but it would be nice if we didn’t have to.
Best Practices
I consider it a best practice to define any property I intend to use at the top of my Ember.Object
. If it will be a simple data type, I use something like:
export default Ember.Object.extend({
aNumber: 0,
aString: "",
anArray: Ember.computed(() => []),
// computed properties...
});
Consequently, I almost never want to get or set a property on an Ember.Object
that I did not explicitly define. Ideally, Ember would tell me immediately if I tried to get or set an unknown property and I didn’t intend to do so.
Strict Models
Luckily, it isn’t hard to accomplish this. Ember.Object
provides two functions, unknownProperty()
and setUnknownProperty()
, which can be used to create “strict” models. The presumed intention of these functions is to allow dynamic execution based on the string value of the property name to be retrieved or set.
However, we can repurpose them to make our Ember.Object
models more strict. We can implement unknownProperty()
and setUnknownProperty()
to throw errors providing details of the property meant to be used, which allows us to fail fast and get as close as Ember allows to a locked down, static class.
// as a mixin - this could also be defined on a base class
export default Ember.Mixin.create({
unknownProperty(key) {
throw new Error(`attempting to get unknown property ${key}`);
},
setUnknownProperty(key,value) {
throw new Error(`attempting to set unknown property '${key}' to '${value}'`);
}
});
Now, our first example throws an error when you try to set the wrong property:
post2.set('body', "Ain't goin' nowhere"); // throws error -
// "attempting to set unknown property 'body' to 'Ain't goin' nowhere'"
post.save();
The Exception that Complicates the Rule
Actually, it isn’t quite so simple. On some occasions, the Ember framework (and potentially plugins used by your application) may invoke properties you don’t care about or want to declare. Accordingly, you’ll have to allow some property names even if they are not explicitly defined. For these exceptions, you’ll want to use the default behavior (return undefined
for get()
, and call defineProperty()
for set()
). This is frustrating, but I don’t think it reduces the value of strict models by an unreasonable amount.
const exceptions = [
'isTruthy', // Ember rendering
'size', 'length', // Ember.isPresent
];
// as a mixin - this could also be defined on a base class
export default Ember.Mixin.create({
unknownProperty(key) {
if (!exceptions.contains(key)) {
throw new Error(`attempting to get unknown property ${key}`);
}
},
setUnknownProperty(key,value) {
if (!exceptions.contains(key)) {
throw new Error(`attempting to set unknown property ${key} to ${value}`);
} else {
// I'd rather call _super() here, but Ember only auto-defines a property
// when there is no implementation of setUnknownProperty(), as opposed to
// making defineProperty the default implementation
Ember.defineProperty(this, key, null, value);
return value;
}
}
});
Types, But Not Too Verbose
On my current project, making all of our data transfer objects into strict models has already helped me catch and fix errors more quickly than I would have otherwise. In general, I’d rather have my code throw an error than fail implicitly or silently. And as I wrote about with generating Ember models from C#, even if JavaScript is not statically typed, I’ll nudge it in that direction when I can do so–as long as it doesn’t make my code overly verbose.
Thanks for the article. This could be a useful technique.
Minor point: in the section titled Best practices you give a code example where you assign an array literal to a property. In general this is not JavaScript best practice as instances will share that property which is almost always not the desired behaviour.
Cheers
Good point. I should know better – I’ve run into that issue before more than once, but it slipped my mind when writing up the example. I’ve updated the post.