Tutorial

How to Build Nested Model-driven Forms in Angular 2

Draft updated on Invalid Date
Default avatar

By Jecelyn Yeen

How to Build Nested Model-driven Forms in Angular 2

This tutorial is out of date and no longer maintained.

Introduction

With the Angular 2’s new forms module, we can build complex forms with even more intuitive syntax.

In this article, we will learn about how to build a nested model-driven form with validation using the latest forms module. If you are new to model-driven forms, please refer to How to Build Model-driven Forms in Angular 2](https://www.digitalocean.com/community/tutorials/using-angular-2s-model-driven-forms-with-formgroup-and-formcontrol) for a basic rundown.

What is a Nested Form?

Let’s say you have a product with a name and a price in your model. You want to create a form for it that allows a user to add a new product. But what if you want to add multiple products in the same form? How would we loop through each user model and validate it?

Nested forms allow us to handle multiple models in a single form.

Let’s dig a little deeper and see how we do it.

Our Demo Application

Here’s what we’ll be building:

  • A form to create a customer
  • Ability for a customer to add multiple addresses

We’re going to split our app up into multiple implementations:

Part 1: Create the customer form

View Angular 2 - Nested Model Driven Form (final) scotch - Part 1 on plnkr

Part 2: Move the group of controls to a new component

Angular 2 - Nested Model Driven Form (final) scotch - Part 2 on plnkr

Introduction

We will build a form to capture customer information based on this interface:

customer.interface.ts
export interface Customer {
    name: string; // required field with minimum 5 characters
    addresses: Address[]; // user can have one or more addresses
}

export interface Address {
    street: string;  // required field
    postcode: string;
}

Requirements

Our app will be able to do the following:

  • add or remove an address
  • view error messages of invalid fields
  • submit the form if all fields are valid

Our end result for the Add Customer form will look like this:

Angular 2 Nested Model-driven Forms

App Setup

Let’s take a look at how our app will be setup. We’ll have our app folder which contains our components as well as our index.html file, stylesheet, and tsconfig.json.

|- app/
    |- address.component.html
    |- address.component.ts
    |- app.component.html
    |- app.component.ts
    |- app.module.ts
    |- main.ts
    |- customer.interface.ts
|- index.html
|- styles.css
|- tsconfig.json

In order to use new forms module, we need to npm install @angular/forms npm package and import the reactive forms module in application module.

  1. npm install @angular/forms --save

Here’s the module for our application app.module.ts:

app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent }   from './app.component';

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

export class AppModule { }

The App Component

Let’s move on to create our app component. app.component.ts is our root component and we will write our component code here.

app.component.ts
import { Component, OnInit } from '@angular/core';
import { Validators, FormGroup, FormArray, FormBuilder } from '@angular/forms';
import { Customer } from './customer.interface';

@Component({
    moduleId: module.id,
    selector: 'app-root',
    templateUrl: 'app.component.html',
})
export class AppComponent implements OnInit {
    public myForm: FormGroup; // our form model

    // we will use form builder to simplify our syntax
    constructor(private _fb: FormBuilder) { }

    ngOnInit() {
    // we will initialize our form here
    }

    save(model: Customer) {
        // call API to save customer
        console.log(model);
    }
}

The HTML View

This is what our HTML view will look like:

app.component.html
<div class="container">
    <h4>Add customer</h4>
    <form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm)">
        <!-- we will place our fields here -->
        <button type="submit" [disabled]="!myForm.valid">Submit</button>
    </form>
</div>

We bind myForm to form group directive. The Submit button will be disabled if the form is not valid. save function will be called when we submit the form.

Part 1: Implementation

Now, let’s initialize our form model and create the functions that allow us to add and remove an address.

app.component.ts
/* ... */
ngOnInit() {
    // we will initialize our form here
    this.myForm = this._fb.group({
            name: ['', [Validators.required, Validators.minLength(5)]],
            addresses: this._fb.array([
                this.initAddress(),
            ])
        });
    }

initAddress() {
        // initialize our address
        return this._fb.group({
            street: ['', Validators.required],
            postcode: ['']
        });
    }

addAddress() {
    // add address to the list
    const control = <FormArray>this.myForm.controls['addresses'];
    control.push(this.initAddress());
}

removeAddress(i: number) {
    // remove address from the list
    const control = <FormArray>this.myForm.controls['addresses'];
    control.removeAt(i);
}

/* ... */

We’re using the form builder, _fb, to create our form.

There are 3 functions available in the form builder:

  • group: construct a new form group. e.g., our myForm and address model
  • array: construct a new form array. e.g., our customer’s list of addresses
  • control: construct a new form control.

Each form control accepts an array. The first parameter is the default value of the control, the second parameter accepts either a validator or an array of validators, and the third parameter is the async validator. Please refer to Angular official documentation for details.

In our example, we’ll assign a list of validators to the name control, and assign a single validator to the street control.

Great! All the necessary functions are created. Let’s now bind our form model to the view.

app.component.html
...
<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm)">
    <!-- we will place our fields here -->

    <!-- name -->
    <div class="form-group">
        <label>Name</label>
        <input type="text" formControlName="name">
        <!--display error message if name is not valid-->
        <small *ngIf="!myForm.controls.name.valid" class="text-danger">
            Name is required (minimum 5 characters).
        </small>
    </div>

    <!-- list of addresses -->
    <div formArrayName="addresses">
        <div *ngFor="let address of myForm.controls.addresses.controls; let i=index">
            <!-- address header, show remove button when more than one address available -->
            <div>
                <span>Address {{i + 1}}</span>
                <span *ngIf="myForm.controls.addresses.controls.length > 1"
                    (click)="removeAddress(i)">
                </span>
            </div>

            <!-- Angular assigns array index as group name by default 0, 1, 2, ... -->
            <div [formGroupName]="i">
                <!-- street -->
                <div>
                    <label>street</label>
                    <input type="text" formControlName="street">
                    <!-- display error message if street is not valid -->
                    <small [hidden]="myForm.controls.addresses.controls[i].controls.street.valid">
                        Street is required
                    </small>
                </div>
                <!-- postcode -->
                <div>
                    <label>postcode</label>
                    <input type="text" formControlName="postcode">
                </div>
            <div>
        </div>
    </div>
    <button type="submit" [disabled]="!myForm.valid">Submit</button>
</form>

...

A few notes here:

  • formControlName directive: the form control name.
  • formArrayName directive: the array name. In our example, we bind addresses to the formArrayName.
  • formGroupName directive: the form group name. Since addresses is an array, Angular assigns the index number as the group name to each of the addresses. Therefore, we’ll bind the index i, to formGroupName.

Part 2: Move address to a new component

Our form is working fine now. But imagine that you have a huge form which consists of a lot of controls, we might need to consider moving each group of controls to a separate component to keep our code neat.

Let’s move our address implementation to a new component.

address.component.ts
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
    moduleId: module.id,
    selector: 'address',
    templateUrl: 'address.component.html'
})
export class AddressComponent {
    // we will pass in address from App component
    @Input('group')
    public addressForm: FormGroup;
}

Now, copy the address implementation from the app component view to the address view.

app.component.html
<div [formGroup]="addressForm">
    <div class="form-group col-xs-6">
        <label>street</label>
        <input type="text" class="form-control" formControlName="street">
        <small [hidden]="addressForm.controls.street.valid" class="text-danger">
            Street is required
        </small>
    </div>
    <div class="form-group col-xs-6">
        <label>postcode</label>
        <input type="text" class="form-control" formControlName="postcode">
    </div>
</div>

...

Import address component to app module

Let’s import the address component to our app module.

app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent }   from './app.component';
import { AddressComponent } from './address.component';

@NgModule({
  imports:      [ BrowserModule, ReactiveFormsModule ],
  declarations: [ AppComponent, AddressComponent ], // add address component here
  bootstrap:    [ AppComponent ]
})

export class AppModule { }

Use address component in app component

We can now replace address with our new address component in app component.

app.component.html
<!-- Angular assigns array index as group name by default 0, 1, 2, ... -->
<div [formGroupName]="i">
    <!-- replace address implementation with our new address component -->
    <address [group]="myForm.controls.addresses.controls[i]"></address>
<div>

Summary

Voilà! With the new forms module, we can use formArray to create a list of controls. We can separate each group of controls to a new component and the validation is still working fine.

That’s it. Happy coding!

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Jecelyn Yeen

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel