Working with Template-driven Forms in Angular

Template-driven forms use the two-way binding capabilities of Angular to update the data model as the template state changes with user input.

Forms are core to every user interactive application. They provide an encapsulation of one or more user input elements such as Textboxes, Choices or Selections and group them into a single output object on submission. They also help us in better validation and transformations if required of the user input, before submitting them to the backend datastore.

Angular provides simple and powerful Forms which cater to different requirements and capabilities. The simplest among these forms are the Template-driven forms.

Template-driven forms are built using a template and directives that the Angular provides. These forms use the two-way binding capabilities of Angular to update the data model as the template state changes with user input.

Template driven forms work by using the directives present in FormsModule:

  • NgModel – keeps track of the user changes on a single input element and updates the underlying model
  • NgForm – keeps track of the changes that happen on all the input elements under it, and provides summary of its group along with actions. As soon as we import FormsModule into our Module, NgForm directive is applied on all the form elements available inside that module.

“Template-driven forms are driven by HTML template and NgForm directives over a two-way binding data Model object”

Creating a Template:

To better understand how Template-driven forms work, let’s start by creating a simple Signup component that encapsulates a user signup functionality.

> ng generate component Signup

The command creates a HTML view, TypeScript class, CSS stylesheet and a SPECS Test class. We use Bootstrap 4 to create a simple form view as follows:

<div class="row">
    <div class="col-md-6 col-md-offset-6 mx-auto bg-light p-4">
        <form class="form">
            <div class="form-group">
                <input type="email" class="form-control" placeholder="user@example.com" required pattern="[^ @]*@[^ @]*">
                <span class="text-danger">invalid Email Address!</span>
            </div>
            <div class="form-group">
                <input type="password" class="form-control" placeholder="Password">
                <span class="text-danger">invalid Password!</span>
            </div>
            <div class="form-group">
                <input type="password" class="form-control"placeholder="Retype Password">
                <span class="text-danger">Passwords don't match!</span>
            </div>
            <button class="btn btn-primary" type="submit">Signup</button>
        </form>
    </div>
</div>

Let’s say the requirement of this signup is as follows:

  • user should enter an email address, password and must retype password given
  • email address and password fields are required and must be valid
  • the password must match the retyped password
  • the signup button is enabled only when the above criteria is satisfied
  • the resultant form data is to be collected in a data model for processing

To implement this, we can make use of Template forms based on the NgModel and NgForm directives. Let’s see how we can do it.

To begin, let’s add a model object in our signup.component.ts, where the final processing on submission happens.

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

  model:any = {};

  constructor() { }

  ngOnInit(): void { }
}

We can now use this model to hold our form data, in the HTML let’s bind the input elements with their respective model properties. We shall also add a “Local Reference” to our Form so that we can access its state anytime.

Binding Form Elements to Data Model:

<div class="row">
    <div class="col-md-6 col-md-offset-6 mx-auto bg-light p-4">
        <form class="form" #signupForm="ngForm">
            <div class="form-group">
                <input 
                    type="email" 
                    class="form-control" 
                    [(ngModel)]="model.email" 
                    placeholder="user@example.com" 
                    name="email" 
                    pattern="[^ @]*@[^ @]*">
                <span class="text-danger">invalid Email Address!</span>
            </div>
            <div class="form-group">
                <input 
                    type="password" 
                    class="form-control" 
                    [(ngModel)]="model.password" 
                    placeholder="Password" 
                    name="password">
                <span class="text-danger">invalid Password!</span>
            </div>
            <div class="form-group">
                <input 
                    type="password" 
                    class="form-control" 
                    [(ngModel)]="model.retypepassword" 
                    placeholder="Retype Password" 
                    name="retypepassword">
                <span class="text-danger">Passwords don't match!</span>
            </div>
            <button class="btn btn-primary" type="submit" [disabled]="!signupForm.form.touched || signupForm.form.invalid">Signup</button>
        </form>
    </div>
</div>

Important:

For this to work, add an import statement for “FormsModule” inside your Module file where this component is registered and add FormsModule to the imports array.

@NgModule({
    declarations: [
        SignupComponent
    ],
    imports: [
        FormsModule,
        CommonModule,
        RouterModule.forChild(routes)
    ]
})
export class AuthModule { }

Otherwise you’d get the below error when running your application:

NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'.

Also, keep in mind that every input element which is now added for NgModel “must” contain a name attribute. Otherwise, you’d get another error:

If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions

Observe the syntax for local referencing the form, using which we can now track the changes that happen on the form and all its underlying controls over the data model and apply dynamic validations and transitions.

#signupForm="ngForm"

For example, we can set the Submit button state to be enabled or disabled based on the form’s validity as:

<button class="btn btn-primary" type="submit" [disabled]="!signupForm.form.touched || signupForm.form.invalid">Signup</button>

Validating Form Fields – EmailAddress, Password and Retype:

Now that we’ve added a validation on the overall form state, we should also be mindful about validating each and every form control and showing respective validation results. For example, on the emailAddress input field, we’ve already applied a few HTML5 validations required, pattern

We can now track the state of emailAddress and see if the input is valid against these HTML5 attributes. To check for validity we can access this specific control from the “Local Reference” of the form we just created before.

<input 
    type="email" 
    class="form-control" 
    [(ngModel)]="model.email" 
    placeholder="user@example.com" 
    name="email" 
    pattern="[^ @]*@[^ @]*"
    required>
<span 
    class="text-danger" 
    *ngIf="signupForm.form.controls.email?.invalid">
    invalid Email Address!</span>

The accessor signupForm.form.controls.email?.invalid seems too long for accessing; we can shorten it up by adding a “Local Reference” to this model.

<input 
    type="email" 
    class="form-control" 
    [(ngModel)]="model.email" 
    placeholder="user@example.com" 
    name="email"
    #email="ngModel"
    pattern="[^ @]*@[^ @]*"
    required>
<span 
    class="text-danger"
    *ngIf="email?.invalid">
    invalid Email Address!</span>

Similarly, for comparing the values of Password and Retype password fields, we can use the same approach and compare their values:

<div class="form-group">
    <input 
        type="password" 
        class="form-control" 
        [(ngModel)]="model.password" 
        placeholder="Password" 
        #password="ngModel" 
        name="password">
    <span 
        class="text-danger"
        *ngIf="password?.invalid"
    >invalid Password!</span>
</div>
<div class="form-group">
    <input 
        type="password" 
        class="form-control" 
        [(ngModel)]="model.retypepassword" 
        placeholder="Retype Password" 
        #retypepassword="ngModel" 
        name="retypepassword">
    <span  
        class="text-danger" 
        *ngIf="retypepassword?.dirty && password?.value != retypepassword?.value"
    >Passwords don't match!</span>
</div>

We have the validation condition as retypepassword?.dirty && password?.value != retypepassword?.value, why? Because, we don’t want to show the error for inequality upfront; instead we show it only when user starts to enter something on the retypepassword field, whose model value is being tracked by the bit retypepassword.dirty and so the thing works.

Submitting Form:

At the final step, we can submit the form in two ways:

  • Override the default app submission behaviour – we can do this by explicitly specifying the event that is raised when user taps on the “submit” button.
<form (ngSubmit)="signup(signupForm)" class="form" #signupForm="ngForm">
 ---- form content ----
</form>

The signup() method in this case can access the form data either from the model object that is present in the signup.component.ts class OR can use the passed in reference of the form which is of type NgForm.

The signup() method looks like below:

signup(signupForm: NgForm) {
    console.log(signupForm.value);
    // do something with the data obtained
}

console-log when the form is submitted:
{email: "abcd@gmail.com", password: "Abcd@1234", retypepassword: "Abcd@1234"}
  • Use a button click event instead of submit – A form event is triggered only when it is “submitted”; or in other words a button of type “submit” is clicked. Alternatively, we can use a button which is of type “button” and call the same method signup() from within, which basically does the same thing.
<button 
    class="btn btn-primary" 
    type="button" 
    [disabled]="!signupForm.form.touched || signupForm.form.invalid"
>Signup</button>

The complete form looks like below:

<div class="row">
    <div class="col-md-6 col-md-offset-6 mx-auto bg-light p-4">
        <form 
            (ngSubmit)="signup(signupForm)" 
            class="form" 
            #signupForm="ngForm">
            <div class="form-group">
                <input 
                    type="email" 
                    class="form-control" 
                    [(ngModel)]="model.email" 
                    placeholder="user@example.com" 
                    #email="ngModel" 
                    name="email" 
                    pattern="[^ @]*@[^ @]*">
                <span  
                    class="text-danger" 
                    *ngIf="email?.invalid">
                        invalid Email Address!</span>
                <!-- 
                #Alternatively#
                <span  
                    class="text-danger" 
                    *ngIf="signupForm.form.controls.email?.invalid">
                        invalid Email Address!</span> -->
            </div>
            <div class="form-group">
                <input 
                    type="password" 
                    class="form-control" 
                    [(ngModel)]="model.password" 
                    placeholder="Password" 
                    #password="ngModel" 
                    name="password">
                <span 
                    class="text-danger" 
                    *ngIf="password?.invalid">
                        invalid Password!</span>
            </div>
            <div class="form-group">
                <input 
                    type="password" 
                    class="form-control" 
                    [(ngModel)]="model.retypepassword" 
                    placeholder="Retype Password" 
                    #retypepassword="ngModel" 
                    name="retypepassword">
                <span 
                    class="text-danger" 
                    *ngIf="retypepassword?.dirty 
                    && password?.value != retypepassword?.value">
                        Passwords don't match!</span>
            </div>
            <button 
                class="btn btn-primary" 
                type="submit" 
                [disabled]="!signupForm.form.touched 
                || signupForm.form.invalid">Signup</button>
        </form>
    </div>
</div>

Final Thoughts:

  • Template-driven forms rely entirely on a data “Model” object which acts as the source for tracking data changes and persistence. The functionality is further enhanced by the use of NgForm and other FormModule based directives that make our lives easier.
  • While this approach works, it is limited to only simpler requirements where we might need to build a form for logins, email newsletters and so on.
  • As forms grow in size and complexity and when require customized validators (such as checking if emailAddress is already registered, for example) Template-driven forms don’t offer much.
  • Since Template forms employ Two-way binding, the data is mutable in this case. Each time form state changes, the underlying data changes as well.

Due to these limitations and scalability issues, we use another kind of Forms for building complex forms called as Reactive Forms which we shall discuss next.


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *