Skip to content

Template Driven Forms

Aakash Goplani edited this page Nov 12, 2019 · 12 revisions

Registering the controls

  • Angular will NOT automatically detect your inputs in this form. To avoid this you need to register controls manually, you need to tell Angular hey within that form element, what should be an actual control of my form!

  • In the template driven approach, you simply pick the input which you want to add as a control and then you add ngModel, like this.

    <input type="text" id="username" class="form-control" required ngModel>
  • This will be enough to tell Angular, that this input is actually a control of form, so ngModel in the end is a directive made available in the forms module.

  • Now for this to work, for this to be recognized as a control in your form, we need to give Angular one other piece of information, the name of this control and we do this by adding the normal HTML attribute name.

    <input type="text" id="username" class="form-control" name="username" required ngModel>
  • Here name is nothing Angular 2 specific, name is the default attribute you can add to any HTML control. And with this, this control will be registered in this JavaScript representation of the form.


Submitting and Using the Form

  • You might think that a good place would be on a click listener on submit button because this is a button we click when we want to submit the form, however this is not the best place to do it.

    • Keep in mind that this button here is type submit, so if we click it as it is placed inside of an HTML form element, something else will happen, the default behavior of HTML will be triggered i.e. if you have a button in a form element, this button will submit the form, will send a request normally but besides that, it will also trigger a JavaScript submit event.
  • Angular takes advantage of this and gives a directive we can place on this form element as a whole, it is called ngSubmit and it actually only gives us one event we can listen to.

    <form (ngSubmit)="onSubmit()">
  • To retrieve form values, we want to get access to the form created by Angular. We can use local references. We could place #form on the form element and now we could access this form element on the form reference in our template.

    <form (ngSubmit)="onSubmit(form)" #form>
  • Keep in mind the form element is kind of a selector for a directive built into Angular which will create JavaScript object automatically, we can get access to it by writing ngForm i.e. #form="ngForm"

  • Finally we could pass form as an argument to the onSubmit() method and print it there.

    <form (ngSubmit)="onSubmit(form)" #form="ngForm">
    onSubmit(form: NgForm): void {
      console.log('success: ', this.form);
    }
  • We also do have a value property and if we expand this, we indeed see a couple of key-value pairs here where we have the names of the controls, so the names we set up here and the name attribute like username and email, we find them here and then the values the user entered.

  • With above approach you can access form only after submitting it. If you have a use case to access form data without submitting it, you can use ViewChild()

    <form (ngSubmit)="onSubmit()" #form="ngForm">
    @ViewChild('form', {static: false}) formData: NgForm;
    someRandomMethod(): void {
      console.log('success: ', this.formData);
    }
  • This is how we can submit such a form and how we can also get access to the form object created by Angular.


Form State

  • Now we had a look at the value property which stores the input of the user in key-value pairs, we have a lot of other properties that allows us to really understand the state of our form.

  • We can see which controls we registered here on the controls object, (e.g. e-mail, secret and username) and each control is of type FormControl, where each control then has a couple of properties, mostly the same properties we have on the overall form though.

  • Dirty for example is true because we changed something about that form. If I reload the page and submit it now, you will see that dirty is false because I didn't type into any input, so therefore of course dirty is false.

  • Disabled would be true if the form was disabled for some reason

  • Invalid is false because we haven't added any validators, so it isn't invalid, it is indeed valid.

  • Valid is true, because the form is valid right now,

  • Touched property is for example to see did we click into some of the fields.

    • The difference to dirty would be that for dirty, we have to change the field, have to change the value of a field, for touch we simply have to click into it and now it would be touched

Adding Validations

  • Whilst you should still validate input on the server as the front-end can always be tricked, you can greatly enhance user experience by also validating the input using Angular, for example we want to make sure that none of the fields here is empty and that the e-mail address actually is a valid e-mail address.

  • Now since we use the template driven approach, we can only add them in the template e.g. we can add required to our username input. Now required is a default HTML attribute you can add to an input, however Angular will detect it, so it acts as a selector for a built-in directive shipping with Angular and it will automatically configure your form , to make sure that now this will be treated as invalid if it is empty.

  • And for the e-mail, there is an e-mail directive you can add.

    <input type="email" id="email" class="form-control" ngModel name="email" required email>
  • Now e-mail is not a built-in HTML attribute, it still is a directive which makes sure that this is actually a valid e-mail address. Keep in mind required is only treated as a selector for an Angular directive here.

  • So now if we save this and I submit it, I can still submit it because we haven't set up anything which would prevent us from doing so but if we have a look at it and check the valid attributes, you see it is false. If only all the fields are defined correctly, the valid state turns to true.

  • Angular tracks the state of this form and correctly informs us or gives us a chance of querying it whether this form is valid or not and actually this does not only work on form level, if we dive into the actual controls, you'll see that on the e-mail control, we also have a valid attribute which is true. So it tracks this on a per control level.

  • If we inspect this e-mail element here in the HTML code, you'll see that it adds a couple of classes like ng-dirty, ng-touched and ng-valid.

  • In e-mail field, if I remove the @ sign here, the ng-invalid was added and ng-valid was removed. So Angular dynamically add some classes, giving us information about the state of the individual control here whether it is dirty or not, whether we did change the initial value, whether it touched or not and whether it is valid or not.

  • Now with that information, we can style these inputs conditionally.

Using Form States

  • Angular tracks the state of each control of the form, whether it's valid and so on and conditionally also adds these CSS classes.

  • To disable the submit button if the form is not valid - bind it to the disabled property which will set the disable state of this button to true or false depending on some condition and that condition would be - to check valid property on form reference:

    <button class="btn btn-primary" type="submit" [disabled]="!form.valid">Submit</button>
  • Angular adds the ng-invalid class to the input fields that are invalid and ng-touched to the fields that are invalid plus have been edited. We can use these CSS class highlight input fields with red border if they are invalid:

    input.ng-invalid.ng-touched {
       border: 1px solid red;
    }
    span.help-block {
       color: red;
    }

Display Error Messages

  • You could also add a warning message (or some help text) below this input here for example please enter a valid value or be more precise - add ngIf to conditionally show this if the input value is wrong. To achieve this though, how do we determine whether this input/control does hold a wrong value.

  • We can use a Bootstrap class, help-block, to make sure that this has the appropriate styling and then we could say please enter a valid e-mail.

  • We only want to show this if an invalid value has been entered into the control associated with this input. A quick and easy way of getting access to the control created by Angular automatically is by adding a local reference to this input element itself and associating this now not with ngForm.

    <input type="email" id="email" class="form-control" ngModel name="email" required email #emailInput="ngModel">
  • Just like the form directive automatically added by Angular when it detects a form element, the ngModel directive here also kind of exposes some additional information about the control it creates for us.

  • With this, we could simply check that if e-mail is valid or not in the helper block we previously added

    <input type="email" id="email" class="form-control" ngModel name="email" required email #emailInput="ngModel">
    <span class="help-block" *ngIf="!emailInput.valid && emailInput.touched">Please enter a valid email</span>

Adding Custom Validations

Article


Set default values in Forms

  • To pre-define values, in dropdown for example, we can change the way we add ngModel. Right now, we add it without property or event binding nor two-way data binding, we can use it together with property binding to accomplish this task.

    defaultQuestion: string = 'pet';
    <select id="secret" class="form-control" [ngModel]="defaultQuestion" name="secret">
      <option value="pet">Your first Pet?</option>
      <option value="teacher">Your first teacher?</option>
    </select>
  • You should definitely keep in mind, that you are not limited to using ngModel without any bindings, you can use one-way binding, property binding to set a default value.


Two way data binding in Forms

  • Sometimes you not only want to pre-populate a default value but you also want to instantly react to any changes like check something or simply repeat whatever the user entered.

  • Right now, everything about this form here only updates once I click submit, then I get this form object where I can retrieve the value.

  • We could add ngModel and use two-way binding on ngModel here and bind it to user input and detect changes as soon as input is entered

    answer: string = '';
    <textarea name="questionAnswer" id="questionAnswer" rows="3" class="form-control" [(ngModel)]="answer"></textarea>
    <span>Your reply: {{ answer }}</span>
  • If I submit the whole form and open up the developer tools, you'll see in the value object, it just updates with every keystroke but of course if we submit it, we will get a snapshot of the value at a point of time we hit submit.

  • Three methods of using ngModel in our forms:

    • No binding to just tell Angular that an input is a control
    • One-way binding to give that control a default value
    • Two-way binding to instantly output it or do whatever you want to do with that value.

Group Form Controls

  • We want to group some things, for example we want to group the secret and the question-answer and the username and e-mail to just have some structure in our object because for a very big form, we might want to have such a structure.

  • Here on our first group, the username and e-mail, I have a wrapping div with the ID userData here. You can simply place another directive on it, the ngModelGroup directive like this and this will now group this into a group of inputs. However ngModelGroup needs to be set equal to a string. So for example, the userData, this will be the key name for this group.

  • You can also get access to the JavaScript representation by again adding a local reference to the element which holds the ngModelGroup directive, here for example userData would be a fitting name and then setting this equal to ngModelGroup i.e. #userData="ngModelGroup". So just like we did before with e-mail which was equal to ngModel, we get access to this JavaScript object and this would allow us to for example output a message if this whole group is not valid.

    <div id="user-data" ngModelGroup="userData" #userData="ngModelGroup">
       <div class="form-group">
          <label for="username">Username</label>
          <input type="text" id="username" class="form-control" ngModel name="username" required #nameInput="ngModel">
          <span class="help-block" *ngIf="!nameInput.valid && nameInput.touched">Please enter a valid username</span>
       </div>
       <button class="btn btn-default" (click)="suggestUserName()" type="button">Suggest an Username</button>
       <div class="form-group">
          <label for="email">Mail</label>
          <input type="email" id="email" class="form-control" ngModel name="email" required email #emailInput="ngModel">
          <span class="help-block" *ngIf="!emailInput.valid && emailInput.touched">Please enter a valid email</span>
       </div>
    </div>
  • So now if I save this with ngModelGroup added, If I enter value here and here and hit submit and we have a look at the value of the form, you'll now see that we have a userData field here which holds another object where we now have e-mail and username. Now not only did we add this extra field in our value, we also now have a different set up here in controls, here we also now have a userData control with all the properties you know on those controls, like valid and so on.

  • If we now simply inspect our HTML code and this div with the ID userData, you'll see that there we got the ng-dirty, ng-touched and valid classes added. So you can now also check the validity of this overall control here.


Handling Radio buttons

games: string[] = ['indoor', 'outdoor', 'not interested'];
defaultGame: string = 'outdoor';
<div class="form-group">
   <label for="game">Select Game</label>
   <div class="radio" *ngFor="let game of games">
      <input type="radio" name="game" [value]="game" [ngModel]="defaultGame">
         <label>{{ game }}</label>
   </div>
</div>
  • With each input type we should be having ngModel on it to make it a control and we can pre-populate one option i.e. default option selected using one-way binding.

Setting and Patching form values

  • If I have a scenario that upon clicking a button, we populate input field with a value. We can easily achieve this using two-way data binding but we also have other simple approach using setValue() method.

  • setValue() allows you to set the value of the whole form and here we need to pass a JavaScript object exactly representing our form.

    <button class="btn btn-default" (click)="suggestUserName()" type="button">Suggest an Username</button>
    suggestUserName() {
      const suggestedName = 'Aakash Goplani';
      this.formData.setValue({
        userData: {
          username: suggestedName,
          email: ''
        },
        secret: '',
        questionAnswer: 'Your answer goes here...',
        game: 'indoor'
      });
    }
  • This approach does have one downside - if we already had some value we enter there and then want to click suggest username, it overwrites all our other content. So this is not necessarily the best approach, however it does show you how you can set the values of all controls with one convenient command, with the setValue() command where you pass an exact copy of JavaScript object representing that form and can overwrite the value of each control.

  • The better approach here would be to just to override/update data of single field and that can be achieved using patchValue() method.

  • In patchValue() method you can pass JavaScript object too, where you only overwrite specific certain controls.

    suggestUserName() {
      const suggestedName = 'Aakash Goplani';
      this.formData.form.patchValue({
        userData: {
          username: suggestedName
        }
      });
    }

Using form data

To capture the data from form and store it into JavaScript Object:

  1. Create a JavaScript object representing form structure or reprinting specific fields that you intend to capture & initialize all values to empty string.
    @ViewChild('form', {static: false}) formData: NgForm;
    user = {
      username: '',
      email: '',
      secret: '',
      questionAnswer: '',
      game: ''
    };
  • When submitting the form, we can access all the fields using value property on the form object:

    onSubmit(): void {
      this.user.username = this.formData.value.userData.username;
      this.user.email = this.formData.value.userData.email;
      this.user.secret = this.formData.value.secret;
      this.user.questionAnswer = this.formData.value.questionAnswer;
      this.user.game = this.formData.value.game;
    }
  • This is how you can extract the data, how you can use it, how you can add a property like submitted to make sure you only display certain sections of the form was submitted and how you in the end use that data.


Resetting form values

  • To reset the form, we can call reset() method that will reset the form i.e. empty all the inputs and reset the state like the valid, touched etc.
    this.formData.reset();

Example

<div class="container">
    <div class="row">
        <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
            <!-- <form (ngSubmit)="onSubmit(form)" #form="ngForm"> -->
            <form (ngSubmit)="onSubmit()" #form="ngForm">
                <div id="user-data" ngModelGroup="userData" #userData="ngModelGroup">
                    <div class="form-group">
                        <label for="username">Username</label>
                        <input type="text" id="username" class="form-control" ngModel name="username" required #nameInput="ngModel">
                        <span class="help-block" *ngIf="!nameInput.valid && nameInput.touched">Please enter a valid username</span>
                    </div>
                    <button class="btn btn-default" (click)="suggestUserName()" type="button">Suggest an Username</button>
                    <div class="form-group">
                        <label for="email">Mail</label>
                        <input type="email" id="email" class="form-control" ngModel name="email" required email #emailInput="ngModel">
                        <span class="help-block" *ngIf="!emailInput.valid && emailInput.touched">Please enter a valid email</span>
                    </div>
                </div>
                <span class="help-block" *ngIf="!userData.valid && userData.touched">**Please enter valid user data**</span>
                <div class="form-group">
                    <label for="secret">Secret Questions</label>
                    <select id="secret" class="form-control" [ngModel]="defaultQuestion" name="secret">
                        <option value="pet">Your first Pet?</option>
                        <option value="teacher">Your first teacher?</option>
                    </select>
                </div>
                 <div class="form-group">
                    <textarea name="questionAnswer" id="questionAnswer" rows="3" class="form-control" [(ngModel)]="answer"></textarea>
                    <span>Your reply: {{ answer }}</span>
                </div>
                <div class="form-group">
                    <label for="game">Select Game</label>
                    <div class="radio" *ngFor="let game of games">
                        <input type="radio" name="game" [value]="game" [ngModel]="defaultGame">
                        <label>{{ game }}</label>
                    </div>
                </div>
                <button class="btn btn-primary" type="submit" [disabled]="!form.valid">Submit</button>
            </form>
        </div>
    </div>
    <div class="row" *ngIf="submitted">
        <hr>
        <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
            User Content:
            Name: {{ user.username }}
            Email: {{ user.email }}

            Secret Question: {{ user.secret }}
            Answer: {{ user.questionAnswer }}

            Game: {{ user.game }}
        </div>
    </div>
</div>  
.container {
    margin-top: 30px;
}

input.ng-invalid.ng-touched {
    border: 1px solid red;
}

.help-block {
    color: red;
}

#user-data.ng-invalid.ng-touched {
    border: 1px solid red;
    padding: 1%;
}

input[type='radio'] {
    margin-left: 0;
}
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-angular-forms',
  templateUrl: './angular-forms.component.html',
  styleUrls: ['./angular-forms.component.css']
})
export class AngularFormsComponent implements OnInit {

  @ViewChild('form', {static: false}) formData: NgForm;
  defaultQuestion: string = 'pet';
  answer: string = '';
  games: string[] = ['indoor', 'outdoor', 'not interested'];
  defaultGame: string = 'outdoor';
  submitted: boolean = false;
  user = {
    username: '',
    email: '',
    secret: '',
    questionAnswer: '',
    game: ''
  };

  constructor() { }

  ngOnInit() {
  }

  suggestUserName() {
    const suggestedName = 'Aakash Goplani';
    console.log('suggestedName');
    /* this.formData.setValue({
      userData: {
        username: suggestedName,
        email: ''
      },
      secret: '',
      questionAnswer: 'Your answer goes here...',
      game: 'indoor'
    }); */
    this.formData.form.patchValue({
      userData: {
        username: suggestedName
      }
    });
  }

  /* onSubmit(event: NgForm): void {
    console.log('success: ', event);
  } */

  onSubmit(): void {
    console.log('success: ', this.formData);
    this.submitted = true;

    this.user.username = this.formData.value.userData.username;
    this.user.email = this.formData.value.userData.email;
    this.user.secret = this.formData.value.secret;
    this.user.questionAnswer = this.formData.value.questionAnswer;
    this.user.game = this.formData.value.game;

    this.formData.reset();
  }

}

Related Topics

  1. Angular Forms
  2. Reactive Forms
Clone this wiki locally