-
Notifications
You must be signed in to change notification settings - Fork 1
Template Driven Forms
-
Template Driven Approach - you simply set up your form in the template (in HTML code) and Angular will automatically infer the structure of your form i.e. which controls your forms has, which inputs and makes it easy for you to get started quickly.
-
Angular infers such forms, create such a JavaScript object for us as it does when using the template driven approach.
-
It is dependent on
FormsModule
, so import this inapp.module
fileimport { FormsModule } from '@angular/forms'; ... imports: [ FormsModule ],
-
With this imported, Angular will actually automatically create a JavaScript representations of the form when it detects a form element in HTML code.
-
Topics Covered:
-
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.
-
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 theform
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 theonSubmit()
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.
-
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
-
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
andng-valid
. -
In e-mail field, if I remove the
@
sign here, theng-invalid
was added andng-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.
-
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 andng-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; }
-
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 - addngIf
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 sayplease 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>
-
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.
-
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 enteredanswer: 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.
-
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, thengModelGroup
directive like this and this will now group this into a group of inputs. HoweverngModelGroup
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 tongModelGroup
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
andvalid
classes added. So you can now also check the validity of this overall control here.
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.
-
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 } }); }
To capture the data from form and store it into JavaScript Object:
- 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.
- 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();
<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();
}
}