Turn anything into an ng-model with ngModelController
Few people realise that ng-model
can turn any interactive component into a fully-working input for use in Angular's form
system. This saves you a heap of time in reinventing wheels - very often I see 100s of lines of codes in controllers managing state that Angular's ng-model
desperately wants to manage for you.
To demo this we'll plug a non-Angular SVG clock into ng-model
.
Try dragging the sun, editing the input and hitting 'start'. As you can see, we'll achieve full ng-model functionality with a very unusual input.
Turning a widget into an input
First off - here's our template. A standard Angular form, with two-way binding between our input and the value of input.time
via ng-model
:
<form name=timeForm>
<svg width=1000 height=1000>
<!-- here we bind our clock to item.time -->
<g clock-input
ng-model=item.time
name=time>
</g>
<!-- view the value of our input -->
<text x=500 y=500>
{{ item.time | date:"shortTime" }}
</text>
<text x=500 y=550 ng-click="toggle()">
{{ ticking ? "Stop" : "Start" }}
</text>
</svg>
</form>
To get this template working we'll next need to:
- create the
clock-input
directive that initialises our clock widget - access the controller of the
ng-model
within our directive - implement two-way binding with our widget via the controller's API
Step 1
We've got a donutClock()
widget to instantiate, so let's write a wrapper directive:
module.directive("clock-input", function clock() {
return {
link: function(scope, el, attrs) {
// initialize the non-angular widget
var clock = donutClock({
onInput: view2model,
el: el[0],
});
function view2model() {
// placeholder
}
}
}
});
As you can see - the code in donutClock
is going to remain ignorant of ng-model
, meaning it's in no way coupled to Angular.
Step 2
To get at the controller instance for our ng-model
, we request it in our directive definition via the require:
key. Without a prefix, it'll ensure an instance of the ng-model
directive is present on the same element as our clock-input
, and pass the ng-model
's controller to our linking function.
module.directive("clock-input", function clock() {
return {
require: "ngModel",
/* ngModel instance passed as forth param to link */
link: function(scope, el, attrs, ngModel) {
// ...
In this post we'll only be using require
with ngModel
, but I recommend reading the docs and thinking about how your own directive controllers could help clean up your code.
Step 3
We're now going to implement two way binding in our link
function: getting view values into the model, and getting updated model values into the view.
ngModelController
is the important API here. It's the abstract representation of an input's logical state, with various APIs for you to hook into the 2-way binding process.
We inform the controller of updated time values from the widget via ngModel.$setViewValue(newValue)
. This'll update the model after it's been parsed and validated, and update the form object's state variables (e.g timeForm.time.$pristine)
.
To control how updates from the model side are presented by the view we override ngModel.$render()
, which is called when a change is detected to the model:
link: function(scope, el, attrs, ngModel) {
// initialize the non-angular widget, and
// pass in our model updating function
var clock = donutClock({
onInput: view2model,
el: el[0],
});
function view2model(value) {
ngModel.$setViewValue(value);
}
// when angular detects a change to the model,
// we update our widget
ngModel.$render = function model2view() {
clock.set(ngModel.$viewValue);
}
}
That's it - the full source has the code for the clock etc.
You can do lots more with ngModelController
- adding validators to the $validators
object, changing how the values are parsed from the view, or formatted for it, with the $parsers
and $formatters
arrays - but that's a story for another blog post.
Three cheers
I really like this API! One API to turn anything - D3 visualisations, React components, random non-Angular widgets, <canvas>
, the audio input or accelerometer - into an ng-model. A great example of a well designed abstraction promoting reuse.
Summary
- wrap components that capture user interactions with a directive and
require
ngModel
- use the
ngModelController
APIs to control how your widget updates the model, and renders updates to its value - via
ngModelController
you can also control formatting of values to display in the widget, and the parsing and validation of values read from it - if you see a heap of
$watch
es in your controller handling the state of user input, think: is there ang-model
missing somewhere?
Leave a comment via Github 💬