Sunday, 24 June 2018

Angular 6 Forms Template Driven Approach

> Click here to view Reactive Approach
> Angular 6 Samples

Environment: MS VS Code, bootstrap 3 and Angular 6

Download Project (zip): SampleFormTemplateDriven

Topics Covered

  • Sample Form
  • FormsModule
  • ngSubmit on Form
  • Access to Form
  • Adding Validation
  • Error Message and Applying Different Color
  • Grouping the controls
  • Working with radio buttons
  • Handling the Suggestions Button -- Using setValue and patchValue
  • Using Form Data and re-setting the form after displaying data

We have a very simple form with a

  • A user name
  • A button for Suggestion
  • A email
  • A secret question
  • A submit button

Sample Form

On the form element we don't have a method and action attributes. This form will not get posted to the server, so no http post. Check app.component.html for more details
     
<form>
<label for="username">Username</label>
<input type="text" id="username" class="form-control">

<button class="btn btn-default" type="button">Suggestion</button>

<label for="email">Mail</label>
<input type="email" id="email" class="form-control">

<label for="secret">Secret Questions</label>
<select id="secret" class="form-control">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>

<button class="btn btn-primary" type="submit">Submit</button>

</form>


FormsModule

In app modules import import { FormsModule } from '@angular/forms'; and then add FormsModule to imports array.

With the above in, angular will create a javascript representation of the
element. Angular will not detect your inputs in this form. We need to register controls manually like input etc.

ngModel on Form Controls and name attribute
Add ngModel without [()] (two way data binding, for now), this will tell angular that this is control of my form. Also we need to give name to the controls. Without these two pieces angular will not detect it as a form control.

For putting in default values, use it as [ngModel]="SomeProperty" without () around ngModel. We are not doing two way databinding at this time.

[ngModel] is one way data binding

[(ngModel)] is two way data binding

ngSubmit on Form

To make the form submitable, go to app.component.ts file and add onSubmit() method. To call this method, add directive (ngSubmit)="onSubmit()" on form element

Access to Form

To get access to the form created by angular in javascript, put a local reference like #f and pass an f as an argument to onSubmit like


Also in the component change the method to onSubmit(form: NgForm).

onSubmit(form: NgForm){
    console.log(form);
  }


After submit we can then take a look at the console to see form and control properties.

We can also use @ViewChild to access the local reference. For this change the form element to


and then create parameter and change the onSubmit method to
 @ViewChild('f') signUpForm: NgForm;

onSubmit(){
    console.log(this.signUpForm);
  }


Best practice is to use first approach and not the second one.

Adding Validation

For validation, since we are using here the template driven approach, we only add the validators to the template.

Add required attribute to user name.

Add required and email attributes to the email field.

Submit the form and take a look at the console. Valid should be true if proper values have been provided for these controls.

We can disable the submit button when the form is not valid by adding [disabled]="!f.valid" to submit button
<button class="btn btn-primary" type="submit" [disabled]="!f.valid">Submit</button>

For the template-driven approach, we need the directives for validation. We can find out their names, by searching for "validator" in the official docs: https://angular.io/api?type=directive - everything marked with "D" is a directive and can be added to our template.

Additionally, we might also want to enable HTML5 validation (by default, Angular disables it). You can do so by adding the ngNativeValidate  to a control in your template.

Error Message and Applying Different Color

We can add a css class to put border around invalid controls. Now this will show border around form loads as well. We only want to show it when submit is clicked so use ng-touched as well.
input.ng-invalid.ng-touched, select.ng-invalid.ng-touched {
    border: 1px solid red;
  }


To display error messages, first put a reference on the email element as #email="ngModel" and then provide an error message in a span.
<input type="email" id="email" name="email" class="form-control" ngModel required email #email="ngModel">
<span class="help-block" *ngIf="!email.valid && email.touched">Please enter a valid email!</span>


Grouping the controls

Angular provided a very straight way of providing grouping for form controls. Just put the controls inside a div tag and apply ngModelGroup="userData" to it. We can even put a local reference on it to get access to the database object.

<div class="form-group" ngModelGroup="userData" #userData="ngModelGroup">
If you take a look at the console and then value or controls, the controls placed inside the above userData will be group together.

Working with radio buttons

Create a genders array in the component
genders = ['male', 'female'];

and then in the template add

<div class="radio" *ngFor="let g of genders">
<label>
<input type="radio" name="gender" ngModel [value]="g" required>
{{ g }}
</label>
</div>

Handling the Suggestions Button -- Using setValue and patchValue

We have a suggestion button. Clicking this will give us some suggestion and will put it in the form control. First attach an event to it as (click)="onSuggestion()"

We can do two databind and input the suggestion but here we'll use alternate way to do it.

One of the way is by using the local reference used on the form which we pick in the component via @ViewChild
@ViewChild('f') signUpForm: NgForm;


and the writing the event code where we will take advantage of setValue method of the local reference and pass to it a javascript code. Make sure that the javascript code is full representation of the html.

//first approach using the reference
  onSuggestion(){
    const suggestedUserName = "Superuser";
   
    this.signUpForm.setValue({
      userData: {
        username: suggestedUserName
      },
      email: '',
      secret: 'teacher',
      gender: 'female'
    });
  }


However, above is not a better approach. The better approach would be to use the "form" object on signUpForm local reference and then use it patchValue method and pass the javascript as before to it. We also have a setValue method available. Use patchValue when setting some fields otherwise use setValue. 

onSuggestion(){
    const suggestedUserName = "Superuser";

    this.signUpForm.form.patchValue({
      userData: {
        username: suggestedUserName
      },
      email: '',
      secret: 'teacher',
      gender: 'female'
    });
  }


Using Form Data and re-setting the form after displaying data


Lets say we want to display the entered data after submit towards the bottom of the template. For this add following html
 
<div *ngIf="isFormSubmitted">
<hr>
<div class="row">
<div class="col-xs-12">
<h3>Form Data</h3>
<p>Username: {{ user.username }}</p>
<p>Email: {{ user.email }}</p>
<p>Secret question: {{ user.secret }}</p>
<p>Gender: {{ user. secretQuestion}}</p>
</div>
</div>
</div>



and then on the component side add

  isFormSubmitted = false;

  //javascript object not related to the form
  user: {
    username: '',
    email: '',
    secretQuestion: '',
    gender: ''
  }


and then update the onSubmit event
 onSubmit(form: NgForm){
    //console.log(form);

    this.isFormSubmitted = true;
   
    this.user.secretQuestion = form.value.secret;
    this.user.gender = form.value.gender;
    this.user.email = form.value.email;
    this.user.username = form.value.userData.username;
   
    form.reset();
  }


or via local reference as
  onSubmit(){
    //console.log(this.signUpForm);

    this.isFormSubmitted = true;
   
    this.user.secretQuestion = this.signUpForm.value.secret;
    this.user.gender = this.signUpForm.value.gender;
    this.user.email = this.signUpForm.value.email;
    this.user.username = this.signUpForm.value.userData.username

    this.signUpForm.reset();
  }


Validation Check on Form

<p *ngIf="!f.valid && f.touched">The form is invalid</p>
<button class="btn btn-primary" type="submit" [disabled]="!f.valid">Submit</button>

When the form is invalid, the message will display above the submit button.