Forms

2.2

Send feedback

A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors.

We’ve all used a form to log in, submit a help request, place an order, book a flight, schedule a meeting, and perform countless other data entry tasks. Forms are the mainstay of business applications.

Any seasoned web developer can slap together an HTML form with all the right tags. It's more challenging to create a cohesive data entry experience that guides the user efficiently and effectively through the workflow behind the form.

That takes design skills that are, to be frank, well out of scope for this guide.

It also takes framework support for two-way data binding, change tracking, validation, and error handling ... which we shall cover in this guide on Angular forms.

We will build a simple form from scratch, one step at a time. Along the way we'll learn how to:

  • Build an Angular form with a component and template
  • Use ngModel to create two-way data bindings for reading and writing input control values
  • Track state changes and the validity of form controls
  • Provide visual feedback using special CSS classes that track the state of the controls
  • Display validation errors to users and enable/disable form controls
  • Share information across HTML elements using template reference variables

Run the .

Template-driven forms

Many of us will build forms by writing templates in the Angular template syntax with the form-specific directives and techniques described in this guide.

That's not the only way to create a form but it's the way we'll cover in this guide.

We can build almost any form we need with an Angular template — login forms, contact forms, pretty much any business form. We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors, conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.

It will be pretty easy because Angular handles many of the repetitive, boilerplate tasks we'd otherwise wrestle with ourselves.

We'll discuss and learn to build a template-driven form that looks like this:

Clean Form

Here at the Hero Employment Agency we use this form to maintain personal information about heroes. Every hero needs a job. It's our company mission to match the right hero with the right crisis!

Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.

If we delete the hero name, the form displays a validation error in an attention-grabbing style:

Invalid, Name Required

Note that the submit button is disabled, and the "required" bar to the left of the input control changed from green to red.

We'll customize the colors and location of the "required" bar with standard CSS.

We'll build this form in small steps:

  1. Create the Hero model class.
  2. Create the component that controls the form.
  3. Create a template with the initial form layout.
  4. Bind data properties to each form control using the ngModel two-way data binding syntax.
  5. Add an ngControl directive to each form input control.
  6. Add custom CSS to provide visual feedback.
  7. Show and hide validation error messages.
  8. Handle form submission with ngSubmit.
  9. Disable the form’s submit button until the form is valid.

Setup

Follow the setup instructions for creating a new project named angular-forms.

Create the Hero model class

As users enter form data, we'll capture their changes and update an instance of a model. We can't lay out the form until we know what the model looks like.

A model can be as simple as a "property bag" that holds facts about a thing of application importance. That describes well our Hero class with its three required fields (id, name, power) and one optional field (alterEgo).

In the lib directory, create the following file with the given content:

lib/hero.dart

class Hero { int id; String name, power, alterEgo; Hero(this.id, this.name, this.power, [this.alterEgo]); String toString() => '$id: $name ($alterEgo). Super power: $power'; }

It's an anemic model with few requirements and no behavior. Perfect for our demo.

The alterEgo is optional, so the constructor lets us omit it: note the [] in [this.alterEgo].

We can create a new hero like this:

var myHero = new Hero( 42, 'SkyDog', 'Fetch any object at any distance', 'Leslie Rollover'); print('My hero is ${myHero.name}.'); // "My hero is SkyDog."

Create a form component

An Angular form has two parts: an HTML-based template and a component class to handle data and user interactions programmatically. We begin with the class because it states, in brief, what the hero editor can do.

Create the following file with the given content:

lib/hero_form_component.dart (v1)

import 'package:angular2/core.dart'; import 'package:angular2/common.dart'; import 'hero.dart'; const List<String> _powers = const [ 'Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer' ]; @Component( selector: 'hero-form', templateUrl: 'hero_form_component.html') class HeroFormComponent { List<String> get powers => _powers; Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet'); bool submitted = false; void onSubmit() { submitted = true; } // TODO: Remove this when we're done String get diagnostic => 'DIAGNOSTIC: $model'; }

There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before.

Understanding this component requires only the Angular concepts covered in previous guides.

  1. The code imports the Angular core library, and the Hero model we just created.
  2. The @Component selector value of "hero-form" means we can drop this form in a parent template with a <hero-form> tag.
  3. The templateUrl property points to a separate file for the template HTML.
  4. We defined dummy data for model and powers, as befits a demo. Down the road, we can inject a data service to get and save real data or perhaps expose these properties as inputs and outputs for binding to a parent component. None of this concerns us now and these future changes won't affect our form.
  5. We threw in a diagnostic property to return a string describing our model. It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later.

Revise app_component.dart

AppComponent is the application's root component. It will host our new HeroFormComponent.

Replace the contents of the starter app version with the following:

lib/app_component.dart

import 'package:angular2/core.dart'; import 'hero_form_component.dart'; @Component( selector: 'my-app', template: '<hero-form></hero-form>', directives: const [HeroFormComponent]) class AppComponent {}

Create an initial HTML form template

Create the new template file with the following contents:

lib/hero_form_component.html

<div class="container"> <h1>Hero Form</h1> <form> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> </div>

That is plain old HTML 5. We're presenting two of the Hero fields, name and alterEgo, and opening them up for user input in input boxes.

The Name <input> control has the HTML5 required attribute; the Alter Ego <input> control does not because alterEgo is optional.

We've got a Submit button at the bottom with some classes on it for styling.

We are not using Angular yet. There are no bindings, no extra directives, just layout.

The container, form-group, form-control, and btn classes come from Twitter Bootstrap. Purely cosmetic. We're using Bootstrap to give the form a little style!

Angular forms do not require a style library

Angular makes no use of the container, form-group, form-control, and btn classes or the styles of any external library. Angular apps can use any CSS library, or none at all.

Let's add the stylesheet. Open index.html and add the following link to the <head>:

web/index.html (bootstrap)

<link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">

Add powers with *ngFor

Our hero must choose one super power from a fixed list of Agency-approved powers. We maintain that list internally (in HeroFormComponent).

We'll add a select to our form and bind the options to the powers list using ngFor, a technique seen previously in the Displaying Data guide.

Add the following HTML immediately below the Alter Ego group:

lib/hero_form_component.html (powers)

<div class="form-group"> <label for="power">Hero Power</label> <select class="form-control" id="power" required> <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select> </div>

This code repeats the <option> tag for each power in the list of powers. The p template input variable is a different power in each iteration; we display its name using the interpolation syntax.

Two-way data binding with ngModel

Running the app right now would be disappointing.

Early form with no binding

We don't see hero data because we are not binding to the Hero yet. We know how to do that from earlier guides. Displaying Data taught us property binding. User Input showed us how to listen for DOM events with an event binding and how to update a component property with the displayed value.

Now we need to display, listen, and extract at the same time.

We could use the techniques we already know, but instead we'll introduce something new: the [(ngModel)] syntax, which makes binding the form to the model super easy.

Find the <input> tag for Name and update it like this:

lib/hero_form_component.html (name)

<input type="text" class="form-control" id="name" required [(ngModel)]="model.name" ngControl="name"> TODO: remove this: {{model.name}}

We added a diagnostic interpolation after the input tag so we can see what we're doing. We left ourselves a note to throw it away when we're done.

Focus on the binding syntax: [(ngModel)]="...".

If we run the app right now and started typing in the Name input box, adding and deleting characters, we'd see them appearing and disappearing from the interpolated text. At some point it might look like this.

ngModel in action

The diagnostic is evidence that values really are flowing from the input box to the model and back again.

That's two-way data binding! For more information about [(ngModel)] and two-way data bindings, see the Template Syntax page.

Notice that we also added an ngControl directive to our <input> tag and set it to "name" which makes sense for the hero's name. Any unique value will do, but using a descriptive name is helpful. Defining an ngControl directive is a requirement when using [(ngModel)] in combination with a form.

Internally Angular creates NgFormControl instances and registers them with an NgForm directive that Angular attached to the <form> tag. Each NgFormControl is registered under the name we assigned to the ngControl directive. We'll talk about NgForm later in this guide.

Let's add similar [(ngModel)] bindings and ngControl directives to Alter Ego and Hero Power. We'll ditch the input box binding message and add a new binding (at the top) to the component's diagnostic property. Then we can confirm that two-way data binding works for the entire hero model.

After revision, the core of our form should look like this:

lib/hero_form_component.html (controls)

{{diagnostic}} <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" ngControl="name"> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo" [(ngModel)]="model.alterEgo" ngControl="alterEgo"> </div> <div class="form-group"> <label for="power">Hero Power</label> <select class="form-control" id="power" required [(ngModel)]="model.power" ngControl="power"> <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select> </div>
  • Each input element has an id property that is used by the label element's for attribute to match the label to its input control.
  • Each input element has a ngControl directive that is required by Angular forms to register the control with the form.

If we run the app now and changed every hero model property, the form might display like this:

ngModel in action

The diagnostic near the top of the form confirms that all of our changes are reflected in the model.

Delete the {{diagnostic}} binding at the top as it has served its purpose.

Track control state and validity

A form isn't just about data binding. We'd also like to know the state of the controls in our form. Each control in an Angular form tracks its own state and makes it available for inspection through the following field members, as documented in the NgControl API:

  • dirty / pristine indicate whether the control's value has changed
  • touched / untouched indicate whether the control has been visited
  • valid reflects the control value's validity

CSS classes reflecting control state

Often, a control's state is used to set CSS classes of the corresponding form element. We can leverage those class names to change the appearance of the control. For our demo, we'll use the following CSS classes:

State Class if true Class if false
Control has been visited ng-touched ng-untouched
Control's value has changed ng-dirty ng-pristine
Control's value is valid ng-valid ng-invalid

These are the CSS classes used by the now deprecated NgControlStatus class. We'll be using them in our demo. Feel free to use these or other CSS classes.

We'll use the following method to set a control's state-dependent CSS class names:

lib/hero_form_component.dart (controlStateClasses)

/// Returns a map of CSS class names representing the state of [control]. Map<String, bool> controlStateClasses(NgControl control) => { 'ng-dirty': control.dirty ?? false, 'ng-pristine': control.pristine ?? false, 'ng-touched': control.touched ?? false, 'ng-untouched': control.untouched ?? false, 'ng-valid': control.valid ?? false, 'ng-invalid': control.valid == false }; // TODO: does this map need to be cached?

We'll add a template reference variable named name to the Name <input> tag, and use it as an argument to controlStateClasses. We'll use the map value returned by this method to bind to the NgClass directive — read more about this directive and its alternatives in the template syntax guide.

Let's temporarily add a template reference variable named spy to the Name <input> tag and use it to display the input's CSS classes.

lib/hero_form_component.html (name)

<input type="text" class="form-control" id="name" required [(ngModel)]="model.name" #name="ngForm" [ngClass]="controlStateClasses(name)" #spy ngControl="name"> TODO: remove this: {{spy.className}}

The spy template reference variable gets bound to the <input> DOM element, whereas the name variable (through the #name="ngForm" syntax) gets bound to the NgModel associated with the input element.

Why "ngForm"? A Directive's exportAs property tells Angular how to link the reference variable to the directive. We set name to "ngForm" because the ngModel directive's exportAs property is "ngForm".

Now run the app, and look at the Name input box. Follow the next four steps precisely:

  1. Look but don't touch.
  2. Click inside the name box, then click outside it.
  3. Add slashes to the end of the name.
  4. Erase the name.

The actions and effects are as follows:

Control State Transition

We should see the following transitions and class names:

Control state transitions

The ng-valid/ng-invalid pair is the most interesting to us, because we want to send a strong visual signal when the values are invalid. We also want to mark required fields. To create such visual feedback, let's add definitions for the ng-* CSS classes.

Delete the #spy template reference variable and the TODO as they have served their purpose.

Add custom CSS for visual feedback

We can mark required fields and invalid data at the same time with a colored bar on the left of the input box:

Invalid Form

We achieve this effect by adding these class definitions to a new forms.css file:

web/forms.css

.ng-valid[required] { border-left: 5px solid #42A948; /* green */ } .ng-invalid { border-left: 5px solid #a94442; /* red */ }

Update the <head> of index.html to include this style sheet:

web/index.html (styles)

<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="forms.css">

Show and hide validation error messages

We can do better. The Name input box is required and clearing it turns the bar red. That says something is wrong but we don't know what is wrong or what to do about it. We can leverage the control's state to reveal a helpful message.

Here's the way it should look when the user deletes the name:

Name required

To achieve this effect we extend the <input> tag with the "is required" message in a nearby <div> which we'll display only if the control is invalid.

Here's an example of adding an error message to the name input box:

lib/hero_form_component.html (excerpt)

<label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" #name="ngForm" [ngClass]="controlStateClasses(name)" ngControl="name"> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div>

We control visibility of the name error message by binding properties of the name control to the message <div> element's hidden property.

<div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div>

In this example, we hide the message when the control is valid or pristine; pristine means the user hasn't changed the value since it was displayed in this form.

This user experience is the developer's choice. Some folks want to see the message at all times. If we ignore the pristine state, we would hide the message only when the value is valid. If we arrive in this component with a new (blank) hero or an invalid hero, we'll see the error message immediately, before we've done anything.

Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change. Hiding the message while the control is "pristine" achieves that goal.

The hero Alter Ego is optional so we can leave that be.

Hero Power selection is required. We can add the same kind of error handling to the <select> if we want, but it's not imperative because the selection box already constrains the power to valid values.

Submit the form with ngSubmit

The user should be able to submit this form after filling it in. The Submit button at the bottom of the form does nothing on its own, but it will trigger a form submit because of its type (type="submit").

A "form submit" is useless at the moment. To make it useful, bind the form's ngSubmit event property to the hero form component's onSubmit() method:

<form (ngSubmit)="onSubmit()" #heroForm="ngForm">

We slipped in something extra there at the end! We defined a template reference variable, #heroForm, and initialized it with the value "ngForm".

The variable heroForm is now a reference to the NgForm directive that governs the form as a whole.

The NgForm directive

What NgForm directive? We didn't add an NgForm directive!

Angular did. Angular creates and attaches an NgForm directive to the <form> tag automatically.

The NgForm directive supplements the form element with additional features. It holds the controls we created for the elements with ngModel and ngControl directives, and monitors their properties including their validity. It also has its own valid property which is true only if every contained control is valid.

We'll bind the form's overall validity via the heroForm variable to the button's disabled property using an event binding. Here's the code:

<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>

If we run the application now, we find that the button is enabled — although it doesn't do anything useful yet.

Now if we delete the Name, we violate the "required" rule, which is duly noted in the error message. The Submit button is also disabled.

Not impressed? Think about it for a moment. What would we have to do to wire the button's enable/disabled state to the form's validity without Angular's help?

For us, it was as simple as:

  1. Define a template reference variable on the (enhanced) form element.
  2. Refer to that variable in a button many lines away.

Toggle two form regions (extra credit)

Submitting the form isn't terribly dramatic at the moment.

An unsurprising observation for a demo. To be honest, jazzing it up won't teach us anything new about forms. But this is an opportunity to exercise some of our newly won binding skills. If you aren't interested, go ahead and skip to this guide's conclusion.

Let's do something more strikingly visual. Let's hide the data entry area and display something else.

Start by wrapping the form in a <div> and bind its hidden property to the HeroFormComponent.submitted property.

lib/hero_form_component.html (excerpt)

<div [hidden]="submitted"> <h1>Hero Form</h1> <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> </form> </div>

The main form is visible from the start because the submitted property is false until we submit the form, as this fragment from the HeroFormComponent shows:

lib/hero_form_component.dart (submitted)

bool submitted = false; void onSubmit() { submitted = true; }

When we click the Submit button, the submitted flag becomes true and the form disappears as planned.

Now the app needs to show something else while the form is in the submitted state. Add the following HTML below the <div> wrapper we just wrote:

lib/hero_form_component.html (excerpt)

<div [hidden]="!submitted"> <h2>You submitted the following:</h2> <div class="row"> <div class="col-xs-3">Name</div> <div class="col-xs-9 pull-left">{{ model.name }}</div> </div> <div class="row"> <div class="col-xs-3">Alter Ego</div> <div class="col-xs-9 pull-left">{{ model.alterEgo }}</div> </div> <div class="row"> <div class="col-xs-3">Power</div> <div class="col-xs-9 pull-left">{{ model.power }}</div> </div> <br> <button class="btn btn-default" (click)="submitted=false">Edit</button> </div>

There's our hero again, displayed read-only with interpolation bindings. This <div> appears only while the component is in the submitted state.

The HTML includes an Edit button whose click event is bound to an expression that clears the submitted flag.

When we click the Edit button, this block disappears and the editable form reappears.

That's as much drama as we can muster for now.

Conclusion

The Angular form discussed in this guide takes advantage of the following framework features to provide support for data modification, validation, and more:

  • An Angular HTML form template.
  • A form component class with an @Component annotation.
  • Handling form submission by binding to the NgForm.ngSubmit event property.
  • Template reference variables such as #heroForm and #name.
  • [(ngModel)] syntax for two-way data binding.
  • The ngControl directive for validation and form element change tracking.
  • The reference variable’s valid property on input controls to check if a control is valid and show/hide error messages.
  • Controlling the submit button's enabled state by binding to NgForm validity.
  • Custom CSS classes that provide visual feedback to users about invalid controls.

Our final project folder structure should look like this:

angular_forms
lib
app_component.dart
hero.dart
hero_form_component.dart
hero_form_component.html
web
forms.css
index.html
main.dart
styles.css
pubspec.yaml

Here’s the code for the final version of the application:

import 'package:angular2/core.dart'; import 'hero_form_component.dart'; @Component( selector: 'my-app', template: '<hero-form></hero-form>', directives: const [HeroFormComponent]) class AppComponent {} class Hero { int id; String name, power, alterEgo; Hero(this.id, this.name, this.power, [this.alterEgo]); String toString() => '$id: $name ($alterEgo). Super power: $power'; } import 'package:angular2/core.dart'; import 'package:angular2/common.dart'; import 'hero.dart'; const List<String> _powers = const [ 'Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer' ]; @Component( selector: 'hero-form', templateUrl: 'hero_form_component.html') class HeroFormComponent { List<String> get powers => _powers; Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet'); bool submitted = false; void onSubmit() { submitted = true; } /// Returns a map of CSS class names representing the state of [control]. Map<String, bool> controlStateClasses(NgControl control) => { 'ng-dirty': control.dirty ?? false, 'ng-pristine': control.pristine ?? false, 'ng-touched': control.touched ?? false, 'ng-untouched': control.untouched ?? false, 'ng-valid': control.valid ?? false, 'ng-invalid': control.valid == false }; // TODO: does this map need to be cached? } <div class="container"> <div [hidden]="submitted"> <h1>Hero Form</h1> <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" #name="ngForm" [ngClass]="controlStateClasses(name)" ngControl="name"> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo" [(ngModel)]="model.alterEgo" #alterEgo="ngForm" [ngClass]="controlStateClasses(alterEgo)" ngControl="alterEgo" > </div> <div class="form-group"> <label for="power">Hero Power</label> <select class="form-control" id="power" required [(ngModel)]="model.power" #power="ngForm" [ngClass]="controlStateClasses(power)" ngControl="power"> <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select> </div> <button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button> </form> </div> <div [hidden]="!submitted"> <h2>You submitted the following:</h2> <div class="row"> <div class="col-xs-3">Name</div> <div class="col-xs-9 pull-left">{{ model.name }}</div> </div> <div class="row"> <div class="col-xs-3">Alter Ego</div> <div class="col-xs-9 pull-left">{{ model.alterEgo }}</div> </div> <div class="row"> <div class="col-xs-3">Power</div> <div class="col-xs-9 pull-left">{{ model.power }}</div> </div> <br> <button class="btn btn-default" (click)="submitted=false">Edit</button> </div> </div> .ng-valid[required] { border-left: 5px solid #42A948; /* green */ } .ng-invalid { border-left: 5px solid #a94442; /* red */ } <!DOCTYPE html> <html> <head> <title>Hero Form</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="forms.css"> <script defer src="main.dart" type="application/dart"></script> <script defer src="packages/browser/dart.js"></script> </head> <body> <my-app>Loading ...</my-app> </body> </html> import 'package:angular2/platform/browser.dart'; import 'package:hero_form/app_component.dart'; void main() { bootstrap(AppComponent); }