In the world of AngularJS, we concentrate so much on the View Model (Scope) that it is not always obvious as to how to consume functionality that reacts to the DOM (Document Object Model). Take, for example, the Uniform plugin in the jQuery ecosystem. The Uniform plugin presents more visually pleasing form elements that synchronize with the underlying DOM elements. But, when those DOM elements are, in turn, reacting to the View Model, it can be tricky to properly time of the Uniform synchronization.
If thinking about ngModel and the DOM state wasn't hard enough, we can throw the ngValue directive into the mix as well. The ngValue directive allows view-model expressions (as opposed to simple string values) to be bound to the value of the ngModel. When we do this, the flow of information goes something like this:
View Model -> ngValue -> ngModel -> Input Element -> Uniform Element(s).
And, if thinking about the flow of data wasn't hard enough, we also have to consider the fact that the underlying mechanism of flow is driven by $watch() bindings that are bound during linking phases that occur at different priorities. And, on top of that, we don't want to do anything that will force the browser to repaint at an inappropriate time.
In order to integrate Uniform with AngularJS, we're going to need a directive. So, let's consider when that directive will execute. Here are the priorities of the other directives involved:
- ngModel - Priority 0 (but links during the Pre-linking phase).
- ngValue - Priority 100.
Remember, directives compile in descending priority order and then link in reverse order. Since we want to give the ngValue directive time to pipe the correct value into the relevant ngModel expression, we want our Uniform directive to link afterwards. As such, we'll give the Uniform plugin a priority of 101.
Once we have our link priority in order, we have to think about when we want to render and update the Uniform state. Typically, we never want to query the state of the DOM in the link function body as this can force a repaint. Instead, we'll want to defer synchronization until a later time. And, since we have to watch for changes in the ngModel expression anyway, we can do all of this inside of a $watch() binding in our directive.
Through trial and error, however, we will discover that synchronizing Uniform directly in our $watch() binding will force a browser repaint. This is not good. As such, we need to further defer the synchronization through the use of scope.$evalAsync(). This way, synchronization will happen at the end of the current $digest cycle.
Now, if we are going to be using scope.$evalAsync() to synchronize the Uniform plugin, the concept of directive priority really becomes moot. As such, you'll see that the code doesn't actually define a priority. There's no sense in worrying about the order of $watch() bindings if the update is being pushed into an async queue.
Finally, the last thing we have to do is teardown the jQuery plugin when the scope of the directive is destroyed. This is true for all jQuery plugins, and can be done inside of the $destroy binding.
NOTE: Many jQuery plugins do not have a teardown() or destroy() method which makes them unsuitable for a Single Page Application (SPA) and is the cause of much memory leakage. This has nothing to do with AngularJS and would be the same for any SPA built with any framework.
Pulling it all together, the code looks like this:
As you can see, there's actually very little code involved in the Uniform plugin. The complicated part about consuming the Uniform plugin is more about timing than anything else - when do we instantiate, when to update, and how do we keep the user experience positive?
At first, I tried to attack this problem by injecting the ngModelController and binding to the $render() method and the $viewChangeListeners collection. But, the timing never quite worked and the logic ended up being more complicated (especially when you need to keep the original $render() binding - by the ngModel directive - in tact). The $watch()-based approach feels like it's the cleanest and the easiest to reason about.
Want to use code from this post? Check out the license.