Skip to main content
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Marc Grabanski
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Marc Grabanski

What A Select $watch() Teaches Me About ngModel And AngularJS

By
Published in Comments (16)

NOTE: This blog post is not well done. I'm having trouble getting at the root of the feelings I am having; and, I'm having trouble extrapolating full ideas from those feelings and observations. Take with a grain of salt.

The two-way data-binding in AngularJS is very powerful, especially when there is a direct mapping of input-data to view-model-data. But, when the data mapping requires some translation, interpretation, or validation, you need to get your hands a little dirty - you need to get more involved in how data changes are propagated within your Controller(s). As this has happened to me, I've been forced to think more deeply about my AngularJS application architecture; and, as a result, I've found that my life is often simplified (in the long term) by creating some indirection between my ngModel-bindings and my domain model.

When I first got into AngularJS, I would often grab an object from my service layer and then wire it directly into my ngModel bindings. Meaning, if I had a "user" and a form input for the user's name, my ngModel binding would simply be, "user.name". This way, when I changed the user, the input value would change automatically; and, if I changed the input value, the user object would be updated automatically. Such is the wonder that is two-way data binding in AngularJS.

Over time, however, I've come to find that this simplistic approach (which I do not mean in a condescending way) has given me some trouble. Namely:

  • It allows data to propagate before being validated. This leaves parts of the view-model in an undesirable state.

  • It makes "resetting" form values harder. If the changes are allowed to propagate through the view-model, then resetting a form necessitates storing a secondary, "backup" copy of the pristine data that you're changing.

  • It [sometimes] allows the "view" to write directly to the $scope (which should be read-only). If your ngModel binds to a key on the $scope, updates to the given input will cause a direct update to the $scope. This will lead to unexpected (and seemingly "bug-like") behavior due to prototypal inheritance of the $scope chain.

  • It makes creating Select menus consisting of complex objects much more complicated, especially when not all Options correspond to a static value.

Now, I want to drive home the fact that the preceding problems do not always become symptomatic. In fact, simple ngModel bindings will often work well, and will work just as you intended. But, I have run into problems often enough to warrant some deeper thinking. And the low-hanging-fruit solution that I've come up with is to create a "form" hash that is used in the ngModel bindings:

$scope.form = { ... ngModel bindings ... };

By using the form hash, our ngModel will always write to the "form", and never to the $scope. This immediately fixes any trouble caused by the prototypal $scope inheritance. Furthermore, it gives our ngModel bindings a "data silo" that is separated from the rest of the view-model. This silo gives us an opportunity to be calculated with our response to data-changes, including any translational efforts that need to be implemented between our form data and our view-model.

The scenario that really got me thinking about all of this was the need to create an AngularJS Select menu that contained both static and dynamic options. In this previous exploration, I looked at translating selected options into view-model changes. However, I forgot to look at the situation from the other side: translating view-model changes into selected-options.

In the following demo, I have a Select menu that needs to mirror a property in my view-model. In order to get the two-way bindings to work, I have to implement $watch() statements on both the "form" value and the view-model value. And as each side changes, the $watch() handlers will work together in order to synchronize all the data.

<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
	<meta charset="utf-8" />

	<title>
		What A Select $watch() Teaches Me About ngModel And AngularJS
	</title>
</head>
<body>

	<h1>
		What A Select $watch() Teaches Me About ngModel And AngularJS
	</h1>

	<p>
		Helena Bonham Carter is {{ helena.quality }}!
	</p>

	<!-- Set value via Select / ngOptions. -->
	<p>
		<select
			ng-model="form.quality"
			ng-options="q.label for q in qualities">
		</select>
	</p>

	<!-- Set value explicitly on data-model. -->
	<p>
		<a ng-click="setStunning()">Stunning</a> or
		<a ng-click="setBeautiful()">Beautiful</a>
	</p>



	<!-- Load jQuery and AngularJS from the CDN. -->
	<script
		type="text/javascript"
		src="//code.jquery.com/jquery-2.0.0.min.js">
	</script>
	<script
		type="text/javascript"
		src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
	</script>
	<script type="text/javascript">


		// Create an application module for our demo.
		var app = angular.module( "Demo", [] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the root of the application.
		app.controller(
			"AppController",
			function( $scope ) {

				// I define the initial state of Helena.
				$scope.helena = {
					name: "Helena",
					quality: "beautiful"
				};

				// I define the possible qualities that can be
				// selected in the dropdown menu.
				$scope.qualities = [
					{
						label: "Beautiful",
						value: "beautiful"
					},
					{
						label: "Stunning",
						value: "simply stunning"
					},
					{
						label: "Dark",
						value: "dark and mysterious"
					},
					{
						label: "Exotic",
						value: "exotic"
					},
					{
						label: "Silly (disabled)",
						value: "silly"
					}
				];

				// I set up the initial form values for the ngModel
				// bindings. This allows me to examine the form values
				// before I *choose* to have the changes propagated
				// throughout my view-model.
				$scope.form = {
					quality: getQualityOptionByValue( "beautiful" )
				};


				// I watch the data-model for changes such that I may
				// synchronize the form values.
				$scope.$watch(
					"helena.quality",
					function( newValue, oldValue ) {

						// Ignore initial setup.
						if ( newValue === oldValue ) {

							return;

						}

						console.log( "$watch: helena.quality changed." );

						// Ignore if form already mirrors new value.
						if ( $scope.form.quality.value === newValue ) {

							return;

						}

						$scope.form.quality = getQualityOptionByValue( newValue );

					}
				);


				// I watch the form for changes such that I may
				// synchronize the data-model.
				$scope.$watch(
					"form.quality",
					function( newValue, oldValue ) {

						// Ignore initial setup.
						if ( newValue === oldValue ) {

							return;

						}

						console.log( "$watch: form.quality changed." );

						// Ignore if the data-model already mirrors
						// the new value defined in the form.
						if ( $scope.helena.quality === newValue.value ) {

							return;

						}

						// Ignore "invalid" form selection. This isn't
						// a likely use-case; however, it does point
						// out a possible benefit of this two-way
						// data-binding gatekeeper.
						if ( newValue.value === "silly" ) {

							// Reset to the old value!!!
							return( $scope.form.quality = oldValue );

						}

						$scope.helena.quality = newValue.value;

					}
				);


				// ---
				// PUBLIC METHODS.
				// ---


				// I define Helena as beautiful.
				$scope.setBeautiful = function() {

					$scope.helena.quality = "beautiful";

				};


				// I define Helena as stunning!
				$scope.setStunning = function() {

					$scope.helena.quality = "simply stunning";

				};


				// ---
				// PRIVATE METHODS.
				// ---


				// I return the select-option with the given value.
				function getQualityOptionByValue( value ) {

					for ( var i = 0 ; i < $scope.qualities.length ; i++ ) {

						if ( $scope.qualities[ i ].value === value ) {

							return( $scope.qualities[ i ] );

						}

					}

					return( null );

				}

			}
		);


	</script>

</body>
</html>

In this particular demo, by creating a data silo for the AngularJS ngModel bindings (ie. $scope.form), it allows me to:

  • Create a Select menu with complex value objects.
  • Validate (and possibly revert) the form data before changing the view-model.
  • Gracefully deal with circular $watch() bindings.

While this approach definitely has a higher upfront cost - the explicit $watch() bindings - I really do feel that it has a higher long-term payoff. Of course, you may not need the $watch() bindings in your individual use-case; simply creating a "$scope.form" hash will give you a lot of value with little overhead.

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

Thank you very much for posting. I had quite some headaches with how selects work, this makes everything a whole lot clearer.

I wonder however what you would think about the approach to add this logic into a directive, so that can keep your controllers as brief as possible.

4 Comments

While creating an abstraction layer between your view-model binding will definitely solve the problem, i feel like it isn't the 'angular' way of doing things.

I think the default ng-options directive is more than sufficient in most cases, and for case that it isn't sufficient, you can create your own directive to do such a similar thing (using selectCtrl's addOption and removeOption methods).

In terms of validation, I think its angularjs' policy that the model is correct at all times, and validation should be handled when the user gives input, either via a directive on the control with ngModel, or by the code itself if the value of the model is set programmatically, as in the case of your setBeautiful methods.

If you've never used a $watch directive for this situation, you will never need to worry about circular bindings.

15,848 Comments

@Clark,

I was not aware that there even was a Select controller. I can't find any documentation on it under the Select area of the AngularJS API. Do you have a link? I have not done too much with directive controllers yet - only a bit for inter-directive communication; but even that was mostly R&D at this point.

As far as a custom directive goes, however, I am not sure I understand what you're saying. It seems that the custom directive would do the same thing that my current Controller is doing. In my mind, the point of a directive is to deal with user interactions and pipe them into the AngularJS context. But in this case, it sounds like you're saying that the directive would basically only deal with "logic", not with interactions?

While it adds some obvious complexity, I don't see anything inherently "un-angular" about $watch() statements observing changes in the $scope.

That said, I suppose I could change my Select a bit here to be:

ng-model="helena.quality"
ng-options="q.value as q.label for q in qualities"

I *think* this would then create an automatic two-way binding for helena.quality and the select menu (since the "q.value" is the value being selected, not the parent hash).

But, in that case, you still have problems with the "disabled" option, which I grant you is a very wierd, very outlier case. But, it is something I have had to deal with before (especially when the change in Select option leads to an *action* rather than a simple change in value).

You've definitely given me some good stuff to think about! AngularJS is marathon, not a sprint to full understanding :D

15,848 Comments

@Bernd,

The Select / ngOptions stuff is definitely the most complicated of the "model" bindings, especially since the documentation on it (in my opinion) is not entirely clear (and is missing a few patterns).

4 Comments

@Ben

The controller for the select directive isn't documented anywhere afaik but you can see the implementation of it here (lines 136-191):

https://github.com/angular/angular.js/blob/master/src/ng/directive/select.js

You can see how it's used in the options directive a bit further below (although I don't recommend how they've gotten hold of the select controller, I'd declare it as a required controller in the directive itself).

As for the custom directive, I think I wasnt very clear. What I meant was a validation directive very much like ng-required. It would, in essence, do exactly as your controller would do, however you'd be able to leverage the errors array in ngModelController to do more intelligent displaying of errors.

When I get a bit more time I could post up a few fiddles with some code clarifying what I mean.

I'm currently writing this on the phone so sorry for any spelling errors.

3 Comments

@Ben,

To specify my question. A made a directive for a date-input. There are two types of date bound to that field: the input field value as string and the model value as object (containing a javascript-date-object among other things). I was wondering if you think, that it makes sense to handle the view-model / model relation there, instead of the controller.

Using angular at the very basic level usually revolves around two-way bindings and directives handling everything, except for very special cases. I think that might be the reason why Clark thinks that your approach doesn't feel like it is the angular way of doing things.

15,848 Comments

@Clark,

I can't believe you wrote that on your phone! I can barely get through a text message without wishing I had a keyboard :D Thanks for the link. I was actually looking in the Select directive just this afternoon because I had to "patch" a bit of the Select directive in the **minified** code in my application. That was fun :) The Select directive kept re-selecting the first option at every $digest due to a bug (at least in 1.0.2, which is what I'm using in my app).

I'll try to think more about this stuff and the validation approach.

15,848 Comments

@Bernd,

If I understand what you're saying, do you mean that you have a value in the input, and that value is "translated" for the model? So, for example, the user could input:

"Tomorrow"

... and that would be translated into "2013/07/19" in the model?

I know that ngModel can be used with other directives and you require the model controller and do the "$setView" and what not... but I have ZERO experience with that. And, even after reading the documentation, I don't really understand how the ngModel controller can be extended in custom directives.

That said, with my lack of understanding in that area, I would personally go with a $watch() on the form field; and then, when it changes, translate the value into my model.

The way I see it, there's nothing "un-angular" about watching data and responding to it; as long as the Controller knows *nothing* about the DOM, I think the philosophy is in-tact.

3 Comments

Thanks again for taking time to answer.

I have a date which as model is an object (bound by the widget I'm implementing). The directive in this case needs to be able to react by setting the validity via

modelCtrl.$setValidity('required', false);

This could be also done in the controller and this kind of validation is something else than the one you're discussing in your example, but I think doing it in the directive is much more comofortable when it comes to the reuse of the directives. I find it very nice to have directives restrain the view-model (date-object and date-string). This could be done optionally by attributes e.g. for a min-age check, an invalid empty value, etc.
Even further, if you restrict the view-model there will be less transformations required when putting the data back to the model (e.g. a service), if at all, so you can focus on a broader application logic inside of your controllers.

Briefly from my point of view, I would generally prefer to use directives to restrict view-model and view-values to keep the controller limited to special cases and the interaction of different components.

This being said about my preferences, I find this post still most interesting.

15,848 Comments

@Bernd,

So far, I have only used the modelCtrl once in production. Most of the time, I'll just use something like this in my Directives:

$scope[ attributes.someReference ] = someValue;

So basically, my Directive just sets the model value referenced by an attribute on the directive. But, honestly, mostly I try to have my directives call inherited methods:

$scope.setSomeValue( someValue );

... rather than try to set values directly. I find that this approach overcomes the problems introduced (occasionally) by Prototypal inheritance.

Anyway, due to my lack of experience with the ngModel controller, I don't want to try to speak to it too much - I'm sure I'll do more to misinform :D

1 Comments

Hi,

I have select that is rendered by ng-options.
Once I select option from the drop down ng-change occur and its working fine.
I'm trying to update the select from another location (div on the same page).once Im clicking on the div, I'm updating the ng-model value but it not affect the select..the selected option is not updated.
I thought to do a trigger on the select but its not working...
Any Idea how to solve it?

Thanks:)

1 Comments

Hi Ben,

I was about to lost all my hopes about how to do data mapping until I saw this article. Nicely written and very clear. Thank you very much.

I have a simple question: what happens when all data changed? For example, you send an ajax request and it becomes like

`$scope.helena = myAjaxResponse.data;`

which changes whole `$scope.helena` object.

However, let's say new data has only different `name` value when you look manually

{
name: "Maria",
quality: "beautiful"
}

So, (from the pedestrian-point-of-view ) only name has changed. How should I handle this kind of situation?

1 Comments

Unexpected value 1px3 parsing r attribute.
I'm getting this error when i try to load data from json to html. can u pls give a solution.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel