I have to relearn Angular’s Form API every time I use it

Kim Win
Kim Win
Feb 2, 2017 · 9 min read

Angular’s ngModelController is suppose to make form handling easier, yet I find myself re-reading the API docs and googling for a tutorial every time I need to use it. The verbiage of the API docs never resonates with me in a way that sticks — and the step-by-step tutorials that I’ve encountered illustrate basic usage, but fall short in giving me a wholistic conceptual model and do not highlight gotchas. In this primer/refresher, I hope to address these pain points.

Pre-conditions: Angular 1.5.x + ngMessages

My Notes!

The Lifecycle within ngModelController

To me, the most elusive aspect of ngModelController is the execution order of its various pipelines. Before we dig into this lifecycle, let’s go over some terminology. Here, I will reference official angular docs and my interpretation of them in the form of KRLs — or Kim’s Reductionist Language — a mental translation that I prefer using because I find it easier to remember.

Model value: form control data

KRL: what I manipulate from the page controller

View value: template representation of the form control data

KRL: what the user sees

What the user enters into the form is not necessarily what you want to manipulate in the page controller. This is where parsers are handy.

$parsers: Array of functions to execute, as a pipeline, whenever the control reads value from the DOM.

KRL: a list of functions that turn user input into a controller-friendly value

Say you pre-populate a form and the data that is returned by the backend does not look like you want it to; you can use formatters to transform the model value into what is displayed to the user.

$formatters: Array of functions to execute, as a pipeline, whenever the model value changes

KRL: a list of functions that take in the controller-friendly value and turn it into a user-friendly value

Finally, you can use validators to block form submission and render error messages for invalid input.

$validators: A collection of validators that are applied whenever the model value changes.

KRL: A map of validator names to functions that take in the model value and view value as arguments. Validators return true if and only if the model value is valid. If a validator returns false, the error is stored in ngModel.$error and ngForm.$error ; ngForm.$invalid is set to true.

These definitions are straightforward enough, but it’s worthwhile to point out how they translate into practice. Take a basic directive that leverages the ngModelController API.

When the template renders…

Step 1 — $formatters run

When the template renders, $formatters run first. I found this behavior curious because I did not explicitly change the model value after I initialized it and by the API definition, formatters only run when the model value has changed… so, what gives? As it turns out, Angular initializes $viewValue and $modelValue for ngModel instances to NaN. Therefore, it registers a change in model value if the consumer initializes it to something else. If $scope.date were initialized to NaN, $formatters would not run first.

Step 2 — $validators run

Similarly to formatters, validators run when the model value changes. Hence, it makes sense that given the model has changed from NaN to our initialized value, validators run next. However, it’s worth noting that model watchers do a shallow compare. This means that if your model value was an object and a property value changed, formatters and validators would not re-run.

Formatters run (step 1) followed by validators (step 2) when the template first renders

Assuming the user manually changes the input, then….

Step 3 — $parsers run

When the user manually changes the input value, parsers translate the new view value into a new model value.

Let me sidetrack for a moment to elaborate on a $parsers gotcha that I encountered. From the docs:

Returning undefined from a parser means a parse error occurred. In that case, no $validators will run and the ngModel will be set to undefined unless ngModelOptions.allowInvalid is set to true. The parse error is stored in ngModel.$error.parse.

The note is inconspicuous and frankly, my eyes glazed over it, but it merits explicit callout.

When I created my form, I gated form submission based on the form.$invalid flag. I was puzzled when my form could not be submitted. From what I could tell, the validators that I registered via $validators each had a useful error message. If my form was invalid, it follows that a message should appear explaining to the user why the form cannot be submitted. Right? Wrong.

My validators did not run because a parser returned undefined. And because I did not realize that angular created $error.parse on behalf of me, I did not think to create anng-message for the parse error.

TL;DR: If you return undefined from a parser, remember that it invalidates your form so make sure you render an appropriate message for $error.parse.

Step 4. $validators run… most of the time

After the model value changes, validators run against the new model value. Makes sense — we have a new model value so we need to re-evaluate the validity of the form field. However, as mentioned in Step 3, validators do NOT run if there is a parse error. So, in reality, a more accurate title for this step is “Step 4: $validators run if there were no parse errors. Otherwise, skip to Step 5.” But that’s a lot of words for a title so I’m sticking with the one I got.

Since I’m talking about validators, I want to point out some feedback that I now keep in mind when creating them.

Here is a validator that I’ve written and am not proud of:

There are 2 things that I dislike about this validator.

First, I’ve overridden the default required validator instead of leveraging $isEmpty. The implication is that ngRequired may no longer work as expected. By overriding required directly, you can get into a state where $error.required is true even if the ngRequired condition dictates that the form field is not required. Here is the codepen to prove it.

To avoid this issue, override $isEmpty.

Note how $isEmpty is used by ngRequired.

Second, I’ve conflated the meaning of required. Not only does the required function check for the presence of min and max, it also checks that the min value is less than max and that both values are numbers. A better approach is to break the additional checks into their own validators. For example,

Note how inRange returns true when $isEmpty is true. This pattern is similar to the implementation of ngMaxlength and was counterintuitive to me. It didn’t click until my colleague pointed out that it makes sense from a functional programming idiomatic perspective to let an error fall through if it is unrelated.

Like, if you go for [a functional programming paradigm], it feels nicer to just have one link in the pipeline that is adaptive and takes input->output as usual

In practice, it means to keep each validator focused and return false only if the check is applicable to that part of the pipeline. Otherwise, return true and let another validator catch the failure.

I’ve mentioned that $validators run if the model value has changed. Now, suppose you have an attribute value that drives the outcome of a validator, like ngMaxlength. A change in the maxlength does not imply a change in the model value . This is a case where $validate is useful — it lets you force re-validation so you can call it when the attribute value changes.

Sample usage of $validate()

One final note on validation: if a validator fails, angular sets its model value to undefined. To avoid this behavior, use ngModelOptions.allowInvalid=true. Unless a parse error occurred, allowInvalid: true persists the invalid model value instead of undefined.

Step 5. $viewChangeListeners run … most of the time

Per docs,

$viewChangeListeners: Array of functions to execute whenever the view value has changed. It is called with no arguments, and its return value is ignored. This can be used in place of additional $watches against the model value.

I was surprised to find that in practice, $viewChangeListeners do not run every time the DOM value has changed.

When there is a parse error, it is possible to have a new ngModel.$viewValue and not run $viewChangeListeners. I thought this was pretty weird, but it actually works as designed. In spite of its name, $viewChangeListeners run when the model value has changed. If the model value remains undefined multiple times in a row, then $viewChangeListeners do not run. You can extrapolate this to mean that $viewChangeListeners DO NOT run when:

  • a parse error occurs consecutively
  • the $validators pipeline returns false and allowInvalid is false

$viewChangeListeners highlights an important distinction — the view value is NOT necessarily the DOM value. The view value is actually the template representation of the current model value. As a matter of fact, $setViewValue exists to persist the DOM value to ngModelController’s internal view value because they can get out of sync.

$setViewValue should be called when a control wants to change the view value; typically, this is done from within a DOM event handler.

What is the internal view value used for? It gets passed through $parsers and $validators to create an internal model value. Yep. Similar to how there are two view values, there are two model values — an internal representation and scope expression (aka the model that the consumer manipulates from their page controller).

$setViewValue is instrumental in ensuring the scope model value stays in sync with DOM changes. $setViewValue stages the updated view value to run through $parsers and $validators and their output is stored as the internal model value, which eventually persists as the scope model value.

Remember how the docs say$formatters run “whenever the model value changes”? Given this information, shouldn’t $formatters run after $setViewValue causes a model value change? Nope. More accurately, $formatters run when the scope model value is different than the internal model value. When $setViewValue causes a change in the internal model value, it makes matching changes to the scope model so it won’t trigger $formatters. However, when the scope model is changed programmatically, the scope model becomes out of sync with the internal model and thus,$formatters run, creating a new internal view value.

BUT how does this internal value turn into a DOM value? Cue $render. After formatters output a new internal view value, $render is called — and $render uses the internal view value to update the DOM.

Rinse and Repeat

Each time the user manually changes the input, Steps 3–5 will run.

When user enters 02–01–201, parsers run (step 3), followed by Step 5 (viewChangeListeners) because parser failed; parser is run consecutively until model value passes parser — then, validator runs (step 4), followed by viewChangeListeners (step 5)

The Editable Canvas

The learnings on Angular’s Form API went into the editable canvas for Catalant’s enterprise platform.

If you’re curious, we were able to get our input mask to support many controls via transclusion — admittedly, this warrants its own article :), but I wanted to give you a flavor of what our templates look like.

{{ ctrl.name }}

Parting thoughts

To be totally honest, the series of surprising and unintuitive behaviors translated into non-trivial time for me… time spent on researching, understanding, debugging and experimenting. ngModelController API is more nuanced than it lets on and that’s why I felt the need to break it down step-by-step.

I hope this helps future consumers of ngModelController.

I’m always curious to hear other people’s thoughts and feedback — feel free to reach out at kim.win.dev@gmail.com

Catalant Technologies

We build technology that enables the world’s best companies to work with top experts for on-demand needs… then, we write about it.

Thanks to Darien Maillet Valentine

Kim Win

Written by

Kim Win

Senior Software Engineer @wistia.

Catalant Technologies

We build technology that enables the world’s best companies to work with top experts for on-demand needs… then, we write about it.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade