codelord.net

Code, Angular, iOS and more by Aviv Ben-Yosef

Advanced ng-model Integration for Bug-free Controls

| Comments

In the last post I explained that developers have a lot to gain by making sure their custom controls work properly with ng-model. That post shows the starting point – making trivial things like the required validation work and having the form’s $valid property take into account custom controls.

But that’s just the tip of the iceberg, and ng-model allows for quite a bit more customization and integration in order to allow writing controls that work as smoothly as builtin ones.

In this post I’ll how to start integrating your control with the NgModelController and make your controls more capable and robust.

Our Starting Position

Let’s keep going with the example from the previous post, which was this very simple component:

1
2
3
4
5
6
angular.module('app').component('myCustomControl', {
  template: '...',
  bindings: {
    value: '=ngModel'
  }
});

A good first step would be to note that we can use this control with the name attribute, since ngModel looks for it:

1
2
3
4
<form name="$ctrl.form">
  <my-custom-control ng-model="someValue" name="foo">
  </my-custom-control>
</form>

By supplying name="foo" we can now access it from the form to make sure it’s valid, e.g. $ctrl.form.foo.$valid.

Changing Values Properly

In order to make our component work seamlessly with ng-change we will need to make sure that whenever the control’s value is changed as a result of a user interaction (not programmatically), we let NgModelController know.

First, we will need to make sure to require the NgModelController, and then, when the user clicks a button, invoke $setViewValue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('app').component('myCustomControl', {
  template: '...',
  require: {
    ngModelCtrl: 'ngModel'
  }
  bindings: {
    value: '=ngModel'
  },
  controller: function() {
    var self = this;
    self.userToggledOn = function() {
      self.ngModelCtrl.$setViewValue(true, 'click');
    }
  }
});

The important bits here are the require definition and the handling of the user’s click in userToggledOn, which calls NgModelController’s $setViewValue. Note that second parameter which lets it know what kind of DOM event triggered the change.

Defining emptiness

In my previous post I showed how required can simply be dropped in and used once ng-model is in place. That’s only the case, though, if your definition of “emptiness” matches the default logic as described in the documentation. But in case your control’s logic doesn’t match this, e.g. your model is an array and emptiness means the array is empty, you should override this behavior to let NgModelController know what you expect.

Inside your controller, after requiring ngModel as shown above, do this:

1
2
3
4
5
6
7
8
function SomeCtrl() {
  var self = this;
  self.$onInit = function() {
    self.ngModelCtrl.$isEmpty = function(value) {
      return !value || value.length === 0;
    };
  };
}

As you can see, we’re overriding the $isEmpty method, which is intended exactly for this purpose.

Also, note I’m making sure to access ngModelCtrl on $onInit, since it will not be defined earlier.

Handling Programatic Changes

An important part of the integration is to make sure the view is changed whenever the model value gets changed programmatically. For example, if the control’s ng-model attribute is a binding from its parent, and the parent changes that value, it usually means that the control should update the UI in order to show this state (e.g. because an update was received from the server).

In those scenarios, NgModelController expects us to override the $render method. NgModelController places a $watch on its value, and calls $render when it needs to change (though, note this watch is a shallow watch. If you’re mutating an object as your model, you will need to trigger it manually).

This would look roughly like so:

1
2
3
4
5
6
7
8
9
function SomeCtrl() {
  var self = this;
  self.$onInit = function() {
    // ... previous stuff
    self.ngModelCtrl.$render = function() {
      // Update the UI according to the self.ngModelCtrl.$viewValue
    }
  };
}

That’s it for now. There’s much more to ng-model, e.g. parsers and formatters that are handy when you want specific validations on inputs, etc. To be updated when I write about it, subscribe in the form below!

“Maintaining AngularJS feels like Cobol 🤷…”

You want to do AngularJS the right way.
Yet every blog post you see makes it look like your codebase is obsolete. Components? Lifecycle hooks? Controllers are dead?

It would be great to work on a modern codebase again, but who has weeks for a rewrite?
Well, you can get your app back in shape, without pushing back all your deadlines! Imagine, upgrading smoothly along your regular tasks, no longer deep in legacy.

Subscribe and get my free email course with steps for upgrading your AngularJS app to the latest 1.6 safely and without a rewrite.

Get the modernization email course!

Comments