Monday, November 4, 2019

Angular Template and Reactive based form input handling

It is a general requirement to access and manage html input element from Angular component typescript code. In simple use case, the html input element can be accessed from typescript code by passing local reference variable from html to typescript as js method parameter, the parameter type is HtmlInputElement.

However, for more complex use case, it is quite often to access the input element as Angular FormControl object. Angular provides both Template based form handling and Reactive based form handling for this purpose. A single component can have both template and reactive based form handling for its html input elements. Angular form classes of FormControl, FormGroup and FormArray are shared by both template and reactive driven forms, they are just created and managed in different ways.

In template based approach, if an input element within a form element has [(ngModel)] directive defined, then angular will automatically creates the FormControl instance, which is transparent from developer's. Within a form, the name attribute of html input element is used to associate the html element to the FormControl instance within NgForm. Usually, the FormControl or NgForm objects are passed as javascript local reference variable parameter to Typescript code, however, they can also be accessed directly from typescript code by using ViewChild for simple use case. Here, the data flow is between the html element and data model defined in ts code and specified by noModel and name attribute, FormControl works internally without developers' explicit access.

In reactive based approach, if an input element has formControl directive defined, or within a FormGroup or FormArray element, then developer can write code to creates the FormControl instance and manage how the html element (based on formControlName or FormControl attribute) is associated with typescript instance, so it let developers directly control the FormControl object without passing it as local reference variable js parameter. This provides more flexible for supporting complex custom validation, so usually, reactive based approach is your best choice.

Note FormControl is a generic type, and is used to represent all types of html input controls. It is html code decides what html input element type is rendered for the formControl. FormControl's construct only takes parameters of initial input text value, and validation method. FormGroup and FormArray are helper collection type to manage FormControl. FormGroup manages its element based on control's name, FormArray manages its elements based on index, basically, the index is used as the name of the control.

When using reactive based form, the module file should import ReactiveFormsModule from "Angular/forms". When using form template based form, the module file should import FormsModule from angular/forms.
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
  imports: [
    CommonModule,
    FormsModule,
ReactiveFormsModule,
    IonicModule,
    AuthPageRoutingModule
  ],

Angular reactive based form handling

For Angular reactive based form handle, you define and create the FormControl instances in the ts code by yourself and do not rely on angular to automatically generate them for you

1. FormControl
Reactive based form uses FormControl to connect html input element with ts FormControl object, the html input element's FormControl directive attribute's name is used to associate with the ts FormControl property with the same name "myReactFormControl".

Html code
      <mat-form-field>
        Reactive form control test
        <input type="text" matInput [formControl]="myReactFormControl"><br>
      </mat-form-field>    
       Output: <span style="color: blue;">{{myReactFormControl.value}}</span>
      <br>
   
TS code
export class AppComponent implements OnInit {
  // form control
  myReactFormControl = new FormControl('');
myFractFromControl.value contains the current value of the input. User has the full control to which input html elements should be mapped to the ts objects.
2. FormGroup  (optional)
Form group are optional and not necessary to use reactive based FormControl functions. They are only for simplifying the logic by grouping controls together, so multiple FormControl instances are wrapped in a single FormGroup instance. However, a standalone FormControl without FormGroup and Form works in the same way.

The html FormGroup attribute is used to associate with the matched FormGroup property with the same name in component's ts file. The individual input html elements contained in parent FormGroup element use the FormControlName attribute to associate with typescript dictionary object based on the key of the controls

Html file
<div [formGroup]="myFormGroup">
        <div>
          <label for="username">Username</label>
          <input type="text" id="username" formControlName="username">
        </div>
        <div>
          <label for="email">email</label>
          <input type="text" id="email" formControlName="email">
        </div>
        <button mat-button (click)="groupButtonClicked()">Click Me</button> <br>
      </div>

TS file
  myFormGroup: FormGroup;
  constructor(private formBuilder: FormBuilder, private dialog: MatDialog) {}
  ngOnInit() {
     this.myFormGroup = new FormGroup({
      username: new FormControl(''),
      email: new FormControl('')
    });
  }

FormGroup can also contains sub FormGroup or FormArray, in that case, the html side needs to use fromGroupName or FormArrayName attribute to indicate both the name and type information. On typescript side, the typescript object must match both the name and type (FormControl, FormArray, or FormGroup) to associate the html object with the right typescript object.

For sub items within a FormArray, on typescript side, they are hold in a ArrayForm, so no name is set explicitly for each sub item, the item will use the array index as its implicit name. However, the name attribute still needs to be set explicitly on html side, each sub item needs to use FormControlName, FormGroupName or FormArrayName attribute to indicate the type of the sub item, and the value of this name attribute must be a integer, so it can be mapped to a particular element in the array based on the index.

2. For validation, reactive based form only needs to set validation rules in the second parameter of FormControl. The validation state can be retrieved by FormGroup.get('controlName') method
export class ReactFormComponent implements OnInit {
  myForm: FormGroup;
  constructor() { }
  ngOnInit() {
     this.myForm = new FormGroup ({
      username: new FormControl(null, Validators.required),
      email: new FormControl(null, [Validators.required, Validators.email])
    });
  }
The validation output can be accessed from FormGroup's property binding variable.
        <form [formGroup]="myForm" (ngSubmit)="onSubmit()">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              formControlName="username"
              class="form-control" required>
            <span style="color: red;" *ngIf="!myForm.get('username').valid && myForm.get('username').touched">
              user name field is invalid.
            </span>
          </div>
          <div class="form-group">
            <label for="email">email</label>
            <input
              type="text"
              id="email"
              formControlName="email"
              class="form-control">
              <span style="color: red;" *ngIf="!myForm.get('email').valid && myForm.get('email').touched">
                  email field is invalid.
              </span>
          </div>

3. update value from ts file
Similar to template based form handling, FormControl.setValue can be called to replace value for FormControl or FormGroup. patchValue can be used to set a value for a single item.

Angular template based form handling

When angular FormModule is imported, html form element is automatically associated to a NgForm instance, there is no need to add NgForm in your code, and you can access the ngForm instance using the local reference variable. Any input elements inside the Form and with the NgModel attribute will automatically be included in the NgForm.controls property for ts code to access.

1. html input element without a form
<div>input 1: <input type="text" ngModel #input1="ngModel"></div><br>
<div>input 2: <input type="text" #input2></div><br>
<div><button (click)="onsubmit(input1, input2)">submit</button></div>

Typescript code
onsubmit(in1: HTMLInputElement, in2: HTMLInputElement) {
console.log('submit clicked', in1, in2);
const s = in1.value;
}

2. When clicking button whose type is 'submit', the submit event can be handled by form's ngSubmit event binding.
    <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data">
          <div class="form-group">
            <label for="lastname">Username</label>
            <input type="text" id="lastname" class="form-control" ngModel name="lastname" required>
            <input type="text" id="firstname" class="form-control" name="firstname" required>
          </div>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>

3. To access the form data, a local reference can be assigned to value of "ngForm" in form template, and then define a viewChild varible associated with the local reference name to access the form data in its value field.
export class FormComponent implements OnInit {
  @ViewChild('f', {static: false}) view: NgForm;
  constructor() { }
  ngOnInit() {
  }
  onSubmit() {
    console.log('onsubmited clicked, ', this.view);
  }
}

4. For any input html elements within form, in order to include itself in ngForm's controls and value property, the element should add ngModel directive attribute, as well as a name attribute. Internally, Angular creates a FormControl object for each input html element and associating the each pair based on name attribute
    <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data">
          <div class="form-group">
            <label for="lastname">Username</label>
            <input type="text" ngModel name="lastname" required>
            <input type="text"  ngModel name="firstname" required>
          </div>
        <button type="submit">Submit</button>
      </form>

5. Using ViewChild local reference variable
In html element, set a local reference variable name to "ngModel" will enable the ts code to access the NgModel variable of this html element
   <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data">
          <div class="form-group">
            <label for="lastname">Username</label>
            <input type="text" id="lastname" class="form-control" ngModel name="lastname" required>
            <input type="text" id="firstname" class="form-control" ngModel name="firstname" 
required #first="ngModel">
            first name is {{firstNameValue.value}}
          </div>

ts code
export class FormComponent implements OnInit {
  @ViewChild('f') view;
  @ViewChild('first') firstNameValue;

6. Two way bind using noModel
in html element, ngModel attribute can be used to associate with a ts property for two way binding using [(ngModel)] or one way binding (from ts property to html element) using [ngModel].
 <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data">
          <div class="form-group">
            <label for="lastname">Username</label>
            <input type="text" id="lastname" class="form-control" ngModel name="lastname" required>
            <input type="text" id="middlename" class="form-control" [(ngModel)]="middleNameValue" 
name="middlename" required >
            middle name is {{middleNameValue}}
            <input type="text" id="firstname" class="form-control" ngModel name="firstname" required #first="ngModel">
            first name is {{firstNameValue.value}}
          </div>

ts code
export class FormComponent implements OnInit {
  @ViewChild('f', {static: false}) view;
  @ViewChild('first', {static: true}) firstNameValue;
  middleNameValue = 'default Midname';

Actually any html input element'value can be directly accessed from ts code with ngModel two way binding, without the need to add the input element in a parent form element.

7. Client side html and ts code validation
Several build-in validation rules are supported by Angular, the detailed information is available at

On the html code, first set the validation rule on the html element
  <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data">
          <div class="form-group">
            <label for="lastname">Username</label>
            <input type="text" id="lastname" class="form-control" ngModel name="lastname" required minlength="5" #lre="ngModel">
            <span class="help-block" style="color:red" *ngIf="lre.errors && lre.errors.required">lastname is required<br></span>
            <span class="help-block" style="color:red" *ngIf="lre.errors && lre.errors.minlength">lastname is min length is 5<br></span>

Then the validation result can be accessed on the html side with local reference variable by ngif as show above.

The validation result can also be accessed on the ts code with the ViewChild variable as shown below
export class FormComponent implements OnInit {
  @ViewChild('lre', {static: true})  lastName: ngModel;
  
  onSubmit() {
    console.log('last name reference variable', this.lastName, this.lastName.errors);
  } 
}

Two way binding is not required for client validation, it only uses local reference variable.

8. Group input elements for better json output
By default, all input elements will have their values included in json object's name-value pair. In order to better organize the json output, the input elements can be organized using  Several build-in validation rules are supported by Angular, the detailed information is available at ngModelGroup attribute. All elements included in a ngModelGroup will be wrapped into a sub object in the output json, the name of the sub object is the name of the ngModelGroup

For example, with the below form
      <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data" ngModelGroup="userData">
          <div class="form-group">
            <label for="lastname">Username</label>
            <input type="text" id="lastname" class="form-control" ngModel name="lastname" required minlength="5" #lre="ngModel">
            <input type="text" id="middlename" class="form-control" [(ngModel)]="middleNameValue" name="middlename" required #middleNameRef>
            <input type="text" id="firstname" class="form-control" ngModel name="firstname" required #first="ngModel">
          </div>
          <div class="form-group">
            <label for="email">email address:</label>
            <input type="email" id="email" class="form-control" ngModel name="email" required email #emaillocalref="ngModel">
            <div class="radio" *ngFor="let gender of genders">
              <label>
                <input type="radio" name="gender" ngModel [value]="gender">{{gender}}
              </label>
            </div>
          </div>
        </div>
        <div class="form-group">
          <label for="secret">Secret Questions</label>
          <select id="secret" class="form-control" ngModel name="secret">
            <option value="pet">Your first Pet?</option>
            <option value="teacher">Your first teacher?</option>
          </select>
        </div>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>

when clicking submit button, the form's json value is in a flat structure as

"{
"lastname":"",
"middlename":"default Midname",
"firstname":"",
"email":"",
"secret":"",
"gender":""
}"

Now we group the lastname, firstname and middlename in a model group as below, and give the group name as "username"
  <form (ngSubmit)="onSubmit()" #f="ngForm">
        <div id="user-data" >
          <div class="form-group" ngModelGroup="username">
            <label for="lastname">Username</label>
            <input type="text" id="lastname" class="form-control" ngModel name="lastname" required minlength="5" #lre="ngModel">
            <span class="help-block" style="color:red" *ngIf="lre.errors && lre.errors.required">lastname is required<br></span>
            <span class="help-block" style="color:red" *ngIf="lre.errors && lre.errors.minlength">lastname is min length is 5<br></span>            
            <input type="text" id="middlename" class="form-control" [(ngModel)]="middleNameValue" name="middlename" required #middleNameRef>
            <input type="text" id="firstname" class="form-control" ngModel name="firstname" required #first="ngModel">
            first name is: {{firstNameValue.value}}, middle name is: {{middleNameValue}}
          </div>
          <button class="btn btn-default" style="background-color: lightgray" type="button" (click)="suggestUserName()">
<span aria-label='Enter search text'>Suggest an Username</span></button>
          <hr>

Then the form's json value is organized as below
"{
   "username":         {"lastname":"laste","middlename":"mid","firstname":"first"},
   "email":"",
    "secret":"",
    "gender":""
}"

9. set form value from ts code
For setting values for all items in the form, call ngForm.setValue() method, and provide form's full json string as parameter.
   setFormData() {
    this.view.setValue({
      username:
        {
          lastname: 'mylast',
          middlename: 'mymid',
          firstname: 'myfirst'
        },
      email: 'myemail@gmail.com',
      secret: '',
      gender: 'female'
   });
  }

For setting a single value of the form, call ngForm.form.patchValue method method, and only provide the json value for that particular html item. The below is an example that only updates user's last name.
  setUserName() {
    console.log('suggest username button clicked');
    this.view.form.patchValue(  {
      username:
      {
        lastname: 'singlevalue'
      }
    });
  }

To include a template based form control in reactive form group, set ngModelOptions to standalone to true as below. 
      <mat-form-field>
        Template form control test
        <input type="text" matInput [(ngModel)]="myTemplateformControlValue" [ngModelOptions]="{standalone: true}">
      </mat-form-field>
      Output: <span style="color: blue;">{{myTemplateformControlValue}}</span>


The ts file only needs to define a string attribute for it.
myTemplateformControlValuestring;

Additional comment

For using html element validation, there is no need to wrap the input html elements in a form, as long as the element has ngModel attribute defined for it, the angular build-in validation rule can work on the element.

The html and ts code can access the detailed validation error using the local reference variable as shown below

Html file
<input type="text" id="lastname" class="form-control" ngModel name="lastname" required minlength="5" #lre="ngModel">
<span class="help-block" style="color:red" *ngIf="lre.errors && lre.errors.required">lastname is required<br></span>
<span class="help-block" style="color:red" *ngIf="lre.errors && lre.errors.minlength">lastname is min length is 5<br></span>
<button type="button" class="btn btn-primary" (click)="onClick(lre)">submit</button>

TS file
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm, NgModel } from '@angular/forms';

export class Cmp2Component {
  // @ViewChild('lre', {static: true})  lastName: NgModel;
  constructor(public activatedRoute: ActivatedRoute) { }

  onClick(e: NgModel) {
    console.log(e);
  }
}

No comments:

Post a Comment