Form Directives and Validation in AngularTS

Complete guide to AngularTS form directives: ng-model, built-in validators, ng-messages, ng-model-options, and form state properties like valid and dirty.

AngularTS treats HTML forms as first-class citizens of the framework. Every <form> element (and ng-form directive) creates a FormController instance that tracks the aggregate validity and dirty state of all its inputs. Each input bound with ng-model gets its own NgModelController that manages the view-to-model pipeline, validation, and CSS class hooks.

The form directive

A plain <form> element is enhanced by AngularTS into a FormController. Give the form a name attribute and the controller is published on the current scope under that name, giving you programmatic access to validation state.

<form name="registrationForm">
  <input type="email" name="email" ng-model="user.email" required />

  <button type="submit" ng-disabled="registrationForm.invalid">Register</button>
</form>
  .controller('RegistrationCtrl', ['$scope', function($scope) {
    $scope.register = function() {
      if ($scope.registrationForm.valid) {
        // submit logic
      }
    };
  }]);

FormController state properties

PropertyTypeDescription
validbooleantrue when all child controls are valid
invalidbooleantrue when any child control is invalid
pristinebooleantrue before any control has been changed
dirtybooleantrue after any control has been changed
submittedbooleantrue after the form’s submit event fires
errorobjectKeys are validator names; values are arrays of failing controls
pendingobjectKeys are validator names with pending async validators

CSS classes on forms

AngularTS automatically toggles these classes on both the <form> element and each ng-model input:

ClassApplied when
ng-pristineControl has not been changed
ng-dirtyControl has been changed
ng-validAll validators pass
ng-invalidAny validator fails
ng-submittedForm has been submitted
ng-touchedInput has been blurred at least once
ng-untouchedInput has never been blurred
ng-pendingAn async validator is in progress
input.ng-invalid.ng-dirty {
  border: 2px solid #ef4444;
}

input.ng-valid.ng-dirty {
  border: 2px solid #22c55e;
}

/* Show error messages only after the form is submitted or field is touched */
.field-error {
  display: none;
}
input.ng-touched.ng-invalid ~ .field-error {
  display: block;
}

ng-form

ng-form creates a nested FormController that tracks its own controls independently. Because browsers do not allow nested <form> elements, ng-form is the standard way to group controls into sub-sections with their own validity state.

<form name="outerForm">
  <!-- Shipping address group -->
  <ng-form name="shippingForm">
    <input name="street" ng-model="shipping.street" required />
    <input name="city" ng-model="shipping.city" required />
    <p ng-if="shippingForm.invalid">Please complete shipping address.</p>
  </ng-form>

  <!-- Billing address group -->
  <ng-form name="billingForm">
    <input name="street" ng-model="billing.street" required />
    <p ng-if="billingForm.invalid">Please complete billing address.</p>
  </ng-form>

  <button ng-disabled="outerForm.invalid">Submit</button>
</form>

The outer form’s valid is false if any nested ng-form is invalid, allowing you to check a single top-level form while still presenting granular errors per section.


Input directives

All standard HTML input types are supported. For <input>, <textarea>, and <select> elements, AngularTS registers type-specific parsers and validators automatically when ng-model is present.

Text / Email / URL

<input
  type="text"
  name="username"
  ng-model="user.username"
  required
  minlength="3"
  maxlength="20"
  ng-pattern="/^[a-z0-9_]+$/"
/>

<input type="email" name="email" ng-model="user.email" required />

<input type="url" name="website" ng-model="user.website" />

Number

<input
  type="number"
  name="age"
  ng-model="user.age"
  min="18"
  max="120"
  required
/>

Parses the input into a JavaScript number. Sets error.number if the value is not numeric, error.min or error.max if out of range.

Checkbox

<!-- Simple boolean checkbox -->
<input type="checkbox" name="agree" ng-model="user.agreedToTerms" required />

Checkbox models use boolean values.

Radio

<label> <input type="radio" ng-model="plan" value="free" /> Free </label>
<label> <input type="radio" ng-model="plan" value="pro" /> Pro </label>
<label>
  <input type="radio" ng-model="plan" value="enterprise" /> Enterprise
</label>
<p>Selected: {{ plan }}</p>

Select

<!-- Simple string options -->
<select name="country" ng-model="user.country" required>
  <option value="">-- Choose a country --</option>
  <option value="us">United States</option>
  <option value="gb">United Kingdom</option>
</select>

<!-- Object options with ng-options -->
<select
  ng-model="selectedRole"
  ng-options="role.id as role.label for role in roles"
>
  <option value="">-- Select a role --</option>
</select>

Built-in validators

required / ng-required

required makes the field mandatory. ng-required accepts an expression so you can make a field conditionally required.

<!-- Conditionally required -->
<input
  type="text"
  ng-model="user.company"
  ng-required="user.accountType === 'business'"
/>

The validator calls NgModelController.isEmpty on the view value. For text inputs, empty means "". For checkboxes, empty means false.

minlength / ng-minlength

ng-model="user.password" ng-minlength="8" />
<p ng-if="form.password.error.minlength">
  Password must be at least 8 characters.
</p>

maxlength / ng-maxlength

ng-model="tweet.text" ng-maxlength="280" />
<p>{{ 280 - tweet.text.length }} characters remaining</p>

pattern / ng-pattern

pattern accepts a regex literal (as an attribute value subject to interpolation). ng-pattern accepts a scope expression that evaluates to a RegExp object or a string.

<input type="text" ng-model="user.phone" pattern="^\d{10}$" />

<!-- Dynamic pattern from scope -->
<input
  type="text"
  ng-model="user.code"
  ng-pattern="validationRules.codePattern"
/>
  codePattern: /^[A-Z]{2}\d{4}$/
};

Note: Avoid the g (global) flag on patterns used with ng-pattern. RegExp objects with the g flag maintain internal state between calls to .test(), which causes alternating pass/fail results.


ng-messages

ng-messages simplifies displaying validation error messages. It watches a form control’s error object and renders the first matching ng-message child (or all matching, with the multiple attribute).

<form name="signupForm">
  <div class="field">
    <label>Email</label>
    <input type="email" name="email" ng-model="user.email" required />

    <div ng-messages="signupForm.email.error" role="alert">
      <p ng-message="required">Email address is required.</p>
      <p ng-message="email">Please enter a valid email address.</p>
    </div>
  </div>
</form>

By default, only the first matching error is shown. Add the multiple attribute to show all failing validators at once:

<div ng-messages="signupForm.password.error">
  <p ng-message="required">Password is required.</p>
  <p ng-message="minlength">At least 8 characters required.</p>
  <p ng-message="pattern">Must contain a number and special character.</p>
</div>

ng-message-default

ng-message-default acts as a fallback shown when none of the specific ng-message keys match but there is at least one truthy error:

<div ng-messages="field.error">
  <p ng-message="pattern">Invalid format.</p>
  <p ng-message-default>This field has an error.</p>
</div>

Reusable message templates

Use ng-messages-include to load a shared message template file:

<div ng-messages="signupForm.email.error">
  <div ng-messages-include="'/partials/common-messages.html'"></div>
  <p ng-message="email">Not a valid email address.</p>
</div>

ng-model-options

ng-model-options controls when the model is updated and how updates are debounced. It can be placed on a form (to apply to all controls) or on individual inputs.

Debounce

Delay model updates by a given number of milliseconds after the user stops typing:

<input
  type="text"
  ng-model="search.query"
  ng-model-options="{ debounce: 300 }"
/>

This fires a search 300ms after the last keystroke rather than on every character.

Update on blur

Only update the model when the input loses focus:

<input
  type="text"
  ng-model="user.username"
  ng-model-options="{ updateOn: 'blur' }"
/>

Combined events

Update immediately on blur, but also debounced on default (which covers all other events):

<input
  type="text"
  ng-model="user.slug"
  ng-model-options="{
         updateOn: 'blur default',
         debounce: { 'default': 500, 'blur': 0 }
       }"
/>

Available options

updateOn

  • Type: string

Space-separated DOM event names that trigger model updates. Use "default" as a placeholder for the control’s standard update event (e.g., "input" for text inputs).

debounce

  • Type: number | object

Milliseconds to wait before committing the view value. Can be an object mapping event names to delays: { 'blur': 0, 'default': 500 }.

allowInvalid

  • Type: boolean

When true, the model is updated even when the value is invalid (normally the model is set to undefined when validation fails).

timezone

  • Type: string

Timezone offset for date/time input parsing, e.g. "UTC" or "+0530".


Complete form example

The following example demonstrates a registration form with multiple input types, validators, debouncing, and ng-messages error display.

<form name="regForm" ng-submit="register()">
  <input name="email" type="email" ng-model="form.email" required />
  <div ng-messages="regForm.email.error" ng-if="regForm.email.touched">
    <p ng-message="required">Email is required.</p>
    <p ng-message="email">Enter a valid email.</p>
  </div>
  <button ng-disabled="regForm.invalid">Create account</button>
</form>
  .controller('RegistrationCtrl', ['$scope', function($scope) {
    $scope.form = {};

    $scope.submitRegistration = function() {
      $scope.regForm.setSubmitted();

      if ($scope.regForm.valid) {
        // Call your API
        console.log('Registering:', $scope.form);
      }
    };
  }]);