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.

JS Bin

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:

  1. create the clock-input directive that initialises our clock widget
  2. access the controller of the ng-model within our directive
  3. 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

Thanks

@charlotteis for awesome proof reading!