Metaprogramming with defineProperty
Published on Mar 16, 2014
Ember provides a way to do most of what you want without resorting to metaprogramming... but it's a nice option to have, just in case.
We're going to have some fun today exploring metaprograming with the defineProperty method. To understand what defineProperty can do, we need to take a step back and see where properties are used. Notice that in our model and in our routes and in our controller that after the extend, there's a hash. Anything in that hash is a property. Here in the model, title, author, date, and body are all properties. In the routes, model is a property. In our controller, is editing, edit, and doneEditing are also properties. Of course, we don't always call them properties. We would call doneEditing a function because a function has been assigned to the doneEditing property. We might call title an attribute because an attribute has been assigned to the title property.
Let's take one of our properties and re-implement it using defineProperty, just so we can show the mechanics of defineProperty. For our purposes today, defineProperty, we'll take in three parameters. Here, it will take in the context, and the second is the property name, and then the third is the value. Then, let's comment this out because we're just going to be using this for reference.
In order to define our property, we're going to need a property to put that functionality on. We'll call this define attributes and it will be a function that will be called "on 'init'." A property that is called "on 'init'" will be run at the initialization of this model so that it's guaranteed to run before the property that we're defining ever gets called. Within this function, were going to call Ember.defineProperty. Then, we're going to give it the context, which is this model. Then, we'll give it the property name which is "title" and then the value, which is "DS.attr". With this done, we can take out how we defined title before and check that it works. We'll refresh this page and the title is there and still responding as an attribute.
Now, let's define all these attributes like this. We'll make an attributes property and we'll put in there all the attributes that we've used: title, author, date, and body. Then, we're going to make a loop over those. We're going to grab the attributes and call "forEach." We'll feed the function inside the attribute and then put our defineProperty within that loop. Of course, it won't work right off the bat. Right now, it's just going to be defining title four times. Let's put the attribute name in place there and then, it's a little tricky because this is now scope to the "forEach." We want it scoped to the model. Here, we're going to put define a variable called model and then we're going to use that in here. At that point, we should be able to replace all our other attributes with the defined properties. When we reload our page, we'll see that it works. It's working just as before even though we've defined these on the fly at runtime.
The case that inspired this screencast was a group of methods that we were defining by hand for every day of the week. There were three such method groups, resulting in 21 total methods. DefineProperty not only reduced the amount of code, but made adding new functionality or changing the current functionality easier.
However, you should be careful when using this method. You'll notice that in Ember Inspector, in the post, it is only recognizing the ID. Ember Inspector doesn't recognize the attributes that we defined at runtime and metaprogramming is notorious for being hard to support with tools. So by being clever, you're giving up some of the ecosystem support. Second, the source code contains a warning. It says "this is a low-level method used by other parts of the API. You almost never want to call this method directly. Instead, you should use 'Ember.mixin()' to define new properties."
Mixins, if you don't know, allow you to define properties on the mixin that get used in whichever class that the mixin is applied to. If you look at the source code of mixin.js, you'll see that, in applyMixin, after doing lots of very clever things, it calls defineProperty on every property defined in your mixin.
This is an example of what Tom Dale and Yehuda Katz spoke about in their recent keynote: using a low level flexible API, like defineProperty, to build a safer, higher level API like mixins. While you can probably think of some places in your project where you can use defineProperty to simplify your code and reduce repetition, remember that it is a powerful tool, so use it responsibly. Happy coding.