Accessors are methods used to get and set the value of an object’s private data. The advantages to using accessors are well documented: future-proofing, behaviour encapsulation, and so on. Aside from these general merits, there are (at least) two Angular-specific arguments for accessor use.
Why Part 1: Template Headaches
If you read our post on partials you might be thinking that all of your scope problems have already been solved. Well unfortunately that’s not the end of the story. Let’s look at a scenario where partial
isn’t enough.
Suppose we have a dropdown menu that renders a list of items.
Now let’s pretend we’re building a music app. By putting the menu in a partial it can be reused for filtering and sorting songs.
This is fairly straightforward: the partial directive inserts the template and binds data to its scope. items
is a list of menu entries, and selected
will store the most recently clicked entry.
In this simple case, the controller is just initializing data. Now when we select an artist from the dropdown we see:
Artist:
What happened? Well, the problem lies in ng-repeat
. It’s one of the built-in directives that creates its own scope. In fact, ng-repeat
instantiates a new scope for each menu item. When you click on the link, selected
gets assigned to the ng-repeat
scope instead of the template scope.
How do we fix it? The easiest way is to change ng-click
to $parent.selected = item
. This works fine for simple cases, but gets unwieldy when you start nesting directives ($parent.$parent.selected
).
Alternatively, you could wrap selected
in an object bound to the template scope and modify it with object.selected = item
. This works because Angular scopes are objects and child scopes inherit prototypically from their parents (in the case of built-in directives anyway). Here is a snippet from the Angular source where it’s done.
When you access a primitive from a template, you’re actually reading an object property. If it’s not a direct property of the object, the property will be returned from the object’s prototype chain. On the other hand, when you set a primitive from a template, you’re writing an object property. If the property exists, it’s overwritten, otherwise it’s defined. That’s why you need a .
in the names of parent properties you want to write.
If you found these last two paragraphs confusing, you’re in good company! A lot has been written on the subject so if you’re interested, you’ll find a better explanation here.
A workaround for the .
requirement is to call a function in ng-click
, rather than directly setting the property.
This time we have to pass select
and selected
to the partial.
For select
we create a simple setter function.
This approach takes a little more code but enables us to change selected
from anywhere in the template.
Why Part 2 – Angular Makes it Easy
Angular recently introduced a new ngModelOptions directive that takes a getterSetter
option.
getterSetter: boolean value which determines whether or not to treat functions bound to ngModel as getters/setters.
This makes it really easy to pass accessor functions to form inputs, and eliminates the need for a .
in ng-model
.
How – A Simple Accessor
Ok, enough of the why, time to see how to use AngularJS accessors.
That’s it! You can see it in action here.
This constructor returns a function that, stores any value passed to it and returns the value when called without an argument.
Let’s update our example to make use accessors. First, switch back to the original template.
Next, some minor tweaks to the partial.
Finally, we instantiate the accessors in the controller.
That was easy. You can also pass Accessor
instances to ngModel
by setting getterSetter: true
in ngModelOptions
.
The End
If you’re here because you were having scope problems, congratulations, you’re done. If you want to unleash the awesome power of AngularJS accessors, check out these bonus features.
Bonus – Drop the $watch
Not this watch, this one. By separating the get
and set
methods, we can override set
to avoid using $watch
in our controllers.
Bonus – Memory
Sometimes it’s useful to keep track of when an attribute has changed. With a few simple functions we can add this ability to our accessor.
Bonus – One Way Only
With a few more minor tweaks, we can limit accessors to either get or set only.
The Good, the Bad, and the Non-performant
Before we put all of these features together into one mighty AngularJS Accessor, we need to talk about the elephant in the room – performance. You might have wondered: why bind all of those prototype functions to fn
? JavaScript doesn’t allow you to subclass Function
and we don’t want to modifyFunction.prototype
, which leaves us two options: bind methods to fn
, or change __proto__
.
Unfortunately, there are significant drawbacks to either approach. Using Chrome’s built-in Heap Profiler, we measured the retained size of an accessor instantiated using each method. Function binding produced an object an order of magnitude larger than the mutated prototype method, and that would only worsen as methods are added. On the other hand, MDN’s __proto__ documentation starts with two big warnings:
Yikes. That put enough fear in us to inspire a simple jsperf test. In the test cases we’ve seen so far, the mutated prototype actually outperformed bound methods, though our test spends a disproportionate amount of time instantiating compared with typical use.
So what does it all mean? Unfortunately, there doesn’t seem to be a silver bullet Accessor
implementation that is perfect for all situations. You need to choose the one that works best for your project. Here are some general guidelines based on what we’ve learned.
- Use the function binding method if you have a small number of Accessor methods, or will not create many accessors.
- Mutating the prototype is appropriate if you are less concerned with speed and willing to sacrifice some browser support.
- If you can live with
accessor.rw()
notation, you can avoid performance and memory penalties by not returning a function fromAccessor
.
A Better GetterSetter
Our final implementation defaults to mutating the prototype unless the browser doesn’t support it or safe: true
is passed in the constructor options. Without further adieu, here it is.