August 3, 2017

Model Driven Forms in Angular 2

Model Driven Forms in Angular 2

Creating a nested model driven form in angular 2 is very simple. You want to create a form for it that allows a user to add a new fields(input, text-area, select etc.) dynamically.

Before starting with it let me explain difference between a template driven form and a model driven form.

Difference between template driven and model driven form

In template driven form we write our logic, validation everything in template itself (i.e html file). Whereas in model driven form all the login, validation are written in the controller side (i.e typescript file).

I will explain you this by creating details of a customer.

Folder structure

--app
-----app.component.ts
-----app.module.ts
-----app.template.html
-----address.component.ts
-----address.template.html
-----phone.component.ts
-----phone.template.html
-----phone-mask.directive.ts
-----customer.interface.ts
-----main.ts
--index.html
--systemjs.config
--tsconfig.json

We will build a form to capture information of the user based on the interface below.

customer.interface.ts

export interface Customer {
    name: string;
    addresses: Address[];
    phones: Phone[];
}

export interface Address {
    street: string;
    postcode: string;
}

export interface Phone {
    phone: string;
    phoneType: string;
}

Here the address and phone fields are going to be dynamic. You can add more number of fields as you want.

Here's the module of our application.

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';
import { PhoneComponent } from './phone.component';
import { PhoneMaskDirective } from './phone-mask.directive';

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

export class AppModule { }

Let's move on to create our app component.

app.component.ts

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

@Component({
    moduleId: module.id,
    selector: 'my-app',
    templateUrl: 'app.template.html',
})
export class AppComponent implements OnInit {
    public myForm: FormGroup;

    constructor(private _fb: FormBuilder) { }
    
    ngOnInit() {
    // we will initialize our form here
    }

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

Implementation

All set! Let's implement our model-driven form.

Initialize the Form Model

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

Here is the shortest way to define a form:

ngOnInit() {
    this.myForm = this._fb.group({
        name: ['', [Validators.required, Validators.minLength(5)]],
        addresses: this._fb.array([]),
        phones: this._fb.array([])
    });
 
    // add address
    this.addAddress();
 
    let pArr = [
        {phone: "(502) 555-1234", phoneType: "sms"}, 
        {phone: "(502) 555-1111", phoneType: "home"}, 
        {phone: "(502) 555-9876", phoneType: "tty"}
    ];
    pArr.forEach(p => this.addPhone(p));
}

Lets create a form using angular form.

<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm)">
    <!-- we will place our fields here -->
    <button type="submit">Submit</button>
</form>

A form is a type of FormGroup. A FormGroup can contain one FormGroup or FormControl. In our case, myForm is a FormGroup. There are 3 functions available in the form builder:

  • group: construct a new form group. E.g. our myForm, address model, phone model.
  • array: construct a new form array. E.g. our customer's list of addresses and phones.
  • 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.

Address and phone fields are going to be as form array as these fields are going to be dynamic, user can create any number of these fields by clicking on the add button.

<!--addresses-->
<div formArrayName="addresses">
    <div *ngFor="let address of myForm.controls.addresses.controls; let i=index" class="panel panel-default">
        <div [formGroupName]="i">
            <address [group]="myForm.controls.addresses.controls[i]"></address>
        </div>
    </div>
</div>
<div>
    <a (click)="addAddress()">
        Add another address +
    </a>
</div>

<!-- phones -->
<div formArrayName="phones">
    <div *ngFor="let phone of myForm.controls.phones.controls; let i = index" class="panel panel-default">
        <div [formGroupName]="i">
            <phone [group]="myForm.controls.phones.controls[i]"></phone>
        </div>
    </div>
</div>
<div>
    <a (click)="addPhone()">
        Add another phone number +
    </a>
</div>

A few notes here:

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

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 and phone 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.template.html',
})
export class AddressComponent {
    @Input('group')
    public adressForm: FormGroup;
}

address.template.html

<div [formGroup]="adressForm">
    <div class="form-group col-xs-6">
        <label>street</label>
        <input type="text" class="form-control" formControlName="street">
    </div>
    <div class="form-group col-xs-6">
        <label>postcode</label>
        <input type="text" class="form-control" formControlName="postcode">
    </div>
</div>

phone.component.ts

import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
    moduleId: module.id,
    selector: 'phone',
    templateUrl: 'phone.template.html'
})
export class PhoneComponent {
    @Input('group')
    public phoneForm: FormGroup;
    public phoneTypes = [
       { value: "home", name: "HOME", desc: "For home phone" },
       { value: "sms", name: "SMS", desc: "For text messaging" },
       { value: "tty", name: "TTY", desc: "For the deaf" },
    ]
    
    onClick(phoneType) {
      console.log(phoneType);
    }
}

phone.template.html

<div [formGroup]="phoneForm">
    <div class="form-group col-xs-6">
        <label>Phone number</label>
        <input type="text" class="form-control" placeholder="Phone" formControlName="phone">
    </div>
    <div class="form-group col-xs-6">
        <label>Phone Type</label>
        <select class="form-control" formControlName="phoneType" (ngModelChange)="onClick($event)">
          <option *ngFor="let type of phoneTypes" value="{{type.value}}" title="{{type.desc}}">{{type.name}}</option>
        </select>
    </div>
</div>

Onclick of the add address or add phone number button, dynamic fields gets pushed.

addAddress() {
    const control = this.myForm.controls['addresses'];
    const addrCtrl = this.initAddress();
 
    control.push(addrCtrl);
}

addPhone(p?: {phone: string, phoneType: string}) {
    const control = this.myForm.controls['phones'];
    const phnCtrl = this.initPhone(p);
 
    control.push(phnCtrl);
}

You can deleted the respective address or the phone field by clicking or the X button.

removeAddress(i: number) {
    const control = this.myForm.controls['addresses'];
    control.removeAt(i);
}

removePhone(i: number) {
    const control = this.myForm.controls['phones'];
    control.removeAt(i);
}

How do we update the form value?

Now, imagine we need to assign default user’s name John to the field.

this.myForm = this._fb.group({
    name: ['John', [ Validators.required, Validators.minLength(5)]]
});

What if John is not a static value? We only get the value from API call after we initialize the form model. We can do this:

(this.myForm.controls['name']).setValue('John', { onlySelf: true });

The form control exposes a function call setValue which we can call to update our form control value.

setValue accept optional parameter. In our case, we pass in { onlySelf: true }, mean this change will only affect the validation of this control and not its parent component.

By default this.myForm.controls['name'] is of type AbstractControl. AbstractControl is the base class of FormGroup and FormControl. Therefore, we need to cast it to FormControl in order to utilize control specific function.

How about updating the whole form model?

It's possible! We can do something like this:

const people = {
    name: 'Jane',
    address: {
        street: 'High street',
        postcode: '94043'
    },
    phone: {
        phone: '(502) 555-1234',
        phoneType: 'SMS'
    }
};

(this.myForm).setValue(people, { onlySelf: true });

Listen to form and controls changes

With reactive forms, we can listen to form or control changes easily. Each form group or form control expose a few events which we can subscribe to (e.g. statusChanges, valuesChanges, etc).

Let say we want to do something every time when any form values changed. We can do this:

subcribeToFormChanges() {
    // initialize stream
    const myFormValueChanges$ = this.myForm.valueChanges;

    // subscribe to the stream 
    myFormValueChanges$.subscribe(x => this.events
        .push({ event: ‘STATUS CHANGED’, object: x }));
}

Then call this function in our ngOnInit().

ngOnInit() {
    // subscribe to form changes 
    this.subcribeToFormChanges();
}

Then display all value changes event in our view.

<div *ngFor="let event of events">
    <pre> {{ event | json }} </pre>
</div>

By clicking on the submit button, the values entered or selected in the fields are pushed as an object. An API call can be called in this function

With the new forms module, we can use formArray to create a list of controls. We can seperate each group of controls to a new component.

Note:

Courtesy: Scotch.io, Stack overflow

demo

No comments:

Post a Comment

Popular Posts

Views