#native_company# #native_desc#
#native_cta#

Model-Driven Forms


Learning Objectives

  • How to create a HTML form with a dynamic select box control.

  • How to define a form model on your component and link it to existing HTML form controls.

Form Setup

Whether we are template-driven or model-driven we need some basic form HTML to begin with.

Important

In model-driven forms, contrary to what you might think, the HTML for our form isn’t automatically created for us.

We still need to write the HTML that represents our form and then explicitly link the HTML form elements to code on our component.

We create a simple Bootstrap form with a first name, last name, email, password and language select box controls.

form setup
<form novalidate>
  <fieldset>
    <div class="form-group">
      <label>First Name</label>
      <input type="text"
             class="form-control">
    </div>

    <div class="form-group">
      <label>Last Name</label>
      <input type="text"
             class="form-control">
    </div>
  </fieldset>

  <div class="form-group">
    <label>Email</label>
    <input type="email"
           class="form-control">
  </div>

  <div class="form-group">
    <label>Password</label>
    <input type="password"
           class="form-control">
  </div>

  <div class="form-group">
    <label>Language</label>
    <select class="form-control">
      <option value="">Please select a language</option>
    </select>
  </div>
</form>

We’ve added the novalidate attribute to the form element, by default browsers perform their own validation and show their own error popups. Since we want to handle the form validation ourselves we can switch off this behaviour by adding novalidate to the form element.

Note

We are using the markup and styles from the Twitter Bootstrap UI framework to structure our form.

Dynamic Select Controls

We want the select box to have a list of languages for the user to select.

We could hard code the languages in the HTML with <option> tags but we want to make the list dynamic so we can easily add more languages later on. so we:

  1. Add an array of languages to our component.

    langs: string[] = [
      'English',
      'French',
      'German',
    ]
  2. Use an NgFor loop to render these as options in the template.

    <select class="form-control">
      <option value="">Please select a language</option>
      <option *ngFor="let lang of langs"
              [value]="lang"> (1)
              {{lang}} (2)
      </option>
    </select>
  1. The options value

  2. The options label

An option has a label and a value. The label is the text the user sees in the select box and the value is the data that’s stored for that label.

If we ask a select box what option has been selected it returns us the value, not the label.

To set the value of our select box we just bind to the input property of our option using the [value] syntax.

Form Model

We represent the form as a model composed of instances of FormGroups and FormControls.

Let’s create the model for our form on our component, like so:

import { FormGroup, FormControl } from '@angular/forms';
.
.
.
class ModelFormComponent implements OnInit {
  myform: FormGroup; (1)

  ngOnInit() {
    myform = new FormGroup({
        name: new FormGroup({ (2)
            firstName: new FormControl(), (3)
            lastName: new FormControl(),
        }),
        email: new FormControl(),
        password: new FormControl(),
        language: new FormControl()
    });
  }
}
1 myform is an instance of FormGroup and represents the form itself.
2 FormGroups can nest inside other FormGroups.
3 We create a FormControl for each template form control.

The myform property is an instance of a FormGroup class and this represents our form itself.

Each form control in the template is represented by an instance of FormControl. This encapsulates the state of the control, such as if it’s valid or invalid and even its current value.

These instances of FormControls nest inside our top-level myform: FormGroup, but what’s interesting is that we can nest FormGroups inside other FormGroups.

In our model we’ve grouped the firstName and lastName controls under a FormGroup called name which itself is nested under our top-level myform: FormGroup.

Like the FormControl instance, FormGroup instances encapsulates the state of all of its inner controls, for example an instance of a FormGroup is valid only if all of its inner controls are also valid.

Linking the Form Model to the Form Template

We have the HTML template for our form and the form model on our component, next up we need to link the two together.

We do this using a number of directives which are found in the ReactiveFormsModule, so let’s import that and add it to the imports on our NgModule.

import { ReactiveFormsModule } from '@angular/forms';

formGroup

Firstly we bind the <form> element to our top-level myform property using the formGroup directive, like so:

<form [formGroup]="myform"> ... </form>

Now we’ve linked the myform model to the form template we have access to our myform model in our template.

The value property of the myform model returns the values of all of the controls as an object. We can use that with the json pipe to output some useful debug information about our form, like so:

<pre>{{myform.value | json}}</pre>

Running our application now prints out the below in the debug pre tag:

{
  "name": {
    "firstName": null,
    "lastName": null
  },
  "email": null,
  "password": null,
  "language": null
}

Initially this seems quite exciting but as we enter values into each of the input fields in our form we would see that the model isn’t getting updated, the values remain null.

That’s because although we’ve linked the form element to the myform model this doesn’t automatically link each form control in the model with each form control in the template, we need to do this explicitly with the formControlName and formGroupName directives.

formGroupName & formControlName

We use the formControlName directive to map each form control in the template with a named form control in the model, like so:

<div class="form-group">
  <label>Email</label>
  <input type="email"
         class="form-control"
         formControlName="email" (1)
         required>
</div>
1 This looks for a model form control called email in the top level of our myform model and links the element to that.

We can also associate a group of template form controls to an instance of a form group on our model using formGroupName directive.

Since our firstName and lastName controls are grouped under a form group called name we’ll do just that.

Important

The only caveat is that in our template the controls we want to group must be surrounded by another element, we’ve surrounded our controls with a fieldset element but it doesn’t need to be called fieldset, could for example be a div.

We then associate the fieldset element with the form group called name in our model like so:

<fieldset formGroupName="name"> ... </fieldset>

Then inside our fieldset element we again use the formControlName directive to map individual form controls in the template to form controls under the form group name in our model.

In the end the template should look like this:

<form [formGroup]="myform"> (1)

	<fieldset formGroupName="name">  (2)
		<div class="form-group">
			<label>First Name</label>
			<input type="text"
			       class="form-control"
			       formControlName="firstName" (3)
			       required>
		</div>

		<div class="form-group">
			<label>Last Name</label>
			<input type="text"
			       class="form-control"
			       formControlName="lastName" (3)
			       required>
		</div>
	</fieldset>


	<div class="form-group">
		<label>Email</label>
		<input type="email"
		       class="form-control"
		       formControlName="email" (4)
		       required>
	</div>

	<div class="form-group">
		<label>Password</label>
		<input type="password"
		       class="form-control"
		       formControlName="password" (4)
		       required>
	</div>

	<div class="form-group">
		<label>Language</label>
		<select class="form-control"
		        formControlName="language" (4)
			<option value="">Please select a language</option>
			<option *ngFor="let lang of langs"
			        [value]="lang">{{lang}}
			</option>
		</select>
	</div>

	<pre>{{myform.value | json}}</pre>
</form>
1 Use formGroup to bind the form to an instance of FormGroup on our component.
2 Use formGroupName to map to a child FormGroup of myform.
3 Use formControlName to bind to an instance of a FormControl, since these form controls are under a formGroupName of name, Angular will try and find the control in under myform['name'].
4 Use formControlName to bind to an instance of a FormControl directly under myform.

Now each form control in the template is mapped to form controls in our model and so as we type into the input elements myform.value updates and our debug section at the bottom prints out the current value of the form.

Summary

In this lecture we created a simple HTML form. We created a form model on our component using the FormGroup and FormControl classes.

Then by using directives such as formGroup, formControlName and formGroupName we linked our HTML form to our form model.

In the next lecture we will learn how to add validators to our forms to give visual feedback when the data entered is invalid.

Listing

Listing 1. main.ts
import { NgModule, Component, Pipe, OnInit } from "@angular/core";
import {
  ReactiveFormsModule,
  FormsModule,
  FormGroup,
  FormControl,
  Validators,
  FormBuilder
} from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

@Component({
  selector: "model-form",
  template: `<!--suppress ALL -->
  <div class="container">
<form novalidate
      [formGroup]="myform"
      (ngSubmit)="onSubmit()">

  <fieldset formGroupName="name">
    <div class="form-group">
      <label>First Name</label>
      <input type="text"
             class="form-control"
             formControlName="firstName">
    </div>

    <div class="form-group">
      <label>Last Name</label>
      <input type="text"
             class="form-control"
             formControlName="lastName">
    </div>
  </fieldset>


  <div class="form-group" [ngClass]="{
    'has-danger': email.invalid && email.dirty,
    'has-success': email.valid && email.dirty
  }">
    <label>Email</label>
    <input type="email"
           class="form-control"
           formControlName="email">

    <pre>Dirty? {{ myform.controls.email.dirty }}</pre>
    <pre>Pristine? {{ myform.controls.email.pristine }}</pre>
    <pre>Touched? {{ myform.controls.email.touched }}</pre>
    <pre>Valid? {{ myform.controls.email.valid }}</pre>
    <pre>Invalid? {{ myform.controls.email.invalid }}</pre>
  </div>

  <div class="form-group" [ngClass]="{
    'has-danger': password.invalid && (password.dirty || password.touched),
    'has-success': password.valid && (password.dirty || password.touched)
  }">
    <label>Password</label>
    <input type="password"
           class="form-control"
           formControlName="password">

    <div class="form-control-feedback"
           *ngIf="password.errors && (password.dirty || password.touched)">
        <p *ngIf="password.errors.required">Password is required</p>
        <p *ngIf="password.errors.minlength">Password must be {{password.errors.minlength.requiredLength}} characters long, we need another {{password.errors.minlength.requiredLength - password.errors.minlength.actualLength}} characters </p>
      </div>

    <pre>{{ password.errors | json }}</pre>
  </div>

  <div class="form-group">
    <label>Language</label>
    <select class="form-control"
            formControlName="language">
      <option value="">Please select a language</option>
      <option *ngFor="let lang of langs"
              [value]="lang">{{lang}}
      </option>
    </select>
  </div>

  <pre>{{myform.value | json}}</pre>

  <button type="submit" class="btn btn-primary" >Submit</button>
</form>
</div>
`
})
class ModelFormComponent implements OnInit {
  langs: string[] = ["English", "French", "German"];
  myform: FormGroup;
  firstName: FormControl;
  lastName: FormControl;
  email: FormControl;
  password: FormControl;
  language: FormControl;

  createFormControls() {
    this.firstName = new FormControl("", Validators.required);
    this.lastName = new FormControl("", Validators.required);
    this.email = new FormControl("", [
      Validators.required,
      Validators.pattern("[^ @]*@[^ @]*")
    ]);
    this.password = new FormControl("", [
      Validators.required,
      Validators.minLength(8)
    ]);
    this.language = new FormControl("", Validators.required);
  }

  createForm() {
    this.myform = new FormGroup({
      name: new FormGroup({
        firstName: this.firstName,
        lastName: this.lastName
      }),
      email: this.email,
      password: this.password,
      language: this.language
    });
  }

  ngOnInit() {
    this.createFormControls();
    this.createForm();
  }

  onSubmit() {
    if (this.myform.valid) {
      console.log("Form Submitted!");
      console.log(this.myform.value);
      this.myform.reset();
    }
  }
}

@Component({
  selector: "app",
  template: `<model-form></model-form>`
})
class AppComponent {}

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [AppComponent, ModelFormComponent],
  bootstrap: [AppComponent]
})
class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

Caught a mistake or want to contribute to the book? Edit this page on GitHub!



Advanced JavaScript

This unique course teaches you advanced JavaScript knowledge through a series of interview questions. Bring your JavaScript to the 2021's today.

Level up your JavaScript now!