Experimenting With ngModel And ngModelController In AngularJS
For the last few years, I've happily consumed the ngModel directive, in AngularJS; but, I've never really thought much about what it does - it just worked. But, ngModel does a lot - way more than I actually use it for, currently. After listening to the Form Validation episode of Adventures in AngularJS, with Kent C. Dodds, I was inspired to try to learn more about it.
At the most basic level, the ngModel directive provides a two-way data-binding mechanism that abstracts the connection between your View-Model and various User Interface (UI) controls. So, for example, it knows how to bind your View-Model to something like an input[type=text]; and, conversely, it knows how to update the Input value when your View-Model changes.
But, this two-way data binding could be accomplished with some straightforward(ish) $watch() and $apply() functions. It's not the two-way data binding that makes ngModel so powerful. As Kent pointed out in his podcast interview, the cool stuff - the powerful stuff - is all the peripheral functionality and the interaction with Form containers. In addition to the two-way data binding, the ngModel directive, the ngModelController, the Form directive, and the FormController all work together to provide a robust feature set related to data entry, formatting, and input validation.
I'm only looking into this for the first time, so I can't really go into much more detail than that. As a first step, though, I wanted to try and create my own Input Control (in the generic sense) that uses ngModel to facilitate two-way data-binding.
The control I'm creating is like a Select. However, rather than providing a dropdown menu of options, you need to click the input in order to cycle through the available values, one item per click. Is this a good User Experience (UX)? Absolutely not. But, this felt like a nice low-barrier-of-entry to examine a few different aspects of the ngModel / ngModelController workflow.
My Directive, bnListToggle, consumes the ngModel directive and requires two additional values: the list over which to iterate and the expression used to render the currently selected value.
At the most basic level, an ngModel consumer has to do two things: override the $render() method and call $setViewValue().
The ngModelController.$render() method is called implicitly when the View-Model has been updated and AngularJS needs your input control to update its own state to mirror the new View-Model value. This method takes no arguments. And, when it's called, you are expected to pull the current View-Model value from ngModelController.$viewValue (to help render the Control HTML).
When the View-Model needs to change, based on a unser's interaction with your control, you have to call the ngModelController.$setViewValue() method and pass-in the new value provided by your control. There are few caveats to this method:
First, it doesn't trigger a $digest automatically. So, just as with any directive, you need to wrap this update in an $apply() call so that AngularJS will know that the View-Model has been updated.
Second, it doesn't implicitly call the $render() method. $render() only gets invoked when the View-Model is changed externally to the ngModel consumer. As such, you will need explicitly update the rendering of your Control after you set the $viewValue.
Third, I found this portion of the ngModelController to be very confusing and downright misleading:
If the new value is an object (rather than a string or a number), we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realise that the object has changed and will not invoke the $parsers and $validators pipelines.
Unless I'm missing something, this description makes no sense. And, if you take a step back, you can see why it's misguided. Given a list of values, taken from the Scope, I'm trying to find the selected one when rendering the Control. If I'm passing a COPY of the selected one back into the View-Model, then I'll never be able to find its reference in the list during the next iteration.
I think this advice would only make sense if the selected value originated entirely from within the Control, rather than from within the Scope. That said, I don't have enough experience with ngModel to know what kind of a use-case that is; it seems like you'd always want to pull an object-based $viewValue out of the Scope.
The other thing you can see in this demo is the implicit interaction with the parent Form directive and the FormController. When we give the form a name, the related FormController is automatically applied to the scope, where we can observe changes to its state. We can use this controller to present validation problems and error messages and more - I'm don't really known the full extent of this interaction yet.
And, one thing that you can't see in the code is the fact that CSS classes are automatically applied to both the input control and the form. Specifically, AngularJS is applying an "ng-dirty" class to both the form and the input control when I change the ngModel binding. This is just one of many CSS classes that automatically get applied during the whole ngModel / form lifecycle.
For a long time, I've known that you could create your own custom ngModel controls. But, I was always quite intimidated by the seeming complexity of it all. Now that I know you only have to implement a little code, it seems rather interesting. It's definitely a feature of AngularJS that I'll be digging into more in the future.
Want to use code from this post? Check out the license.
After this experiment, I was inspired to try creating a flexible HTML-based Select menu using ngModel:
This creates a happy medium between the native Select/ngOptions directive and an "old school" HTML dropdown menu.