Angular Forms (Template Driven, Reactive)

angular forms template driven reactive

This article on Angular Forms is part of the Learning Angular series.

A typical structure how Angular handles forms

{
    value: {
        name: 'Murari',
        email: 'someone@gmail.com'
    },
    valid: true
}

Template Driven (TD) vs Reactive Approach

Template Driven – Angular infers the Form Object from the DOM.
Reactive Approach – Form is created programmatically as well as in the HTML, giving more granular control over the form.

TD: Creating the Form and Registering the Controls

Import the FormsModule in the AppComopnent imports
This will make sure that Angular is able to parse any form tag that is available in the HTML.
Considering an example form:

<form>
    <div>
        <label for="username">Username: </label>
        <input id="username" type="text" />
    </div>
    <div>
        <label for="email">Email: </label>
        <input id="email" type="text" />
    </div>
    <button type="submit">SUBMIT</button>
</form>

This will create a javascript form representation in Angular. But the form representation would be blank even if there are elements inside the form tag. This is because, at that point of time Angular hasn’t been informed about which controls are to be included in the form. Even if Angular would be able to parse the form and know about the controls, but not necessarily all the controls in the form need to be submitted.
So to make Angular know about which controls it should consider as to be part of the form, we need to specify them. This can be done as

<input id="username" [(ngModel)]="myname" name="myname" type="text" />
// ngModel -> This tells Angular to include this as part of the form. Tnis is like 2-way binding but a little advanced. Will talk about later.
// name="myname" -> This is the name by which Angular specifically recognises the control. name attribute is a part of the HTMLElement and is nothing special about Angular

Note: To know the structural details of the form, it needs to be submitted

TD: Submitting and Using the Form

Create an onSubmit function in TS file to be called when the user needs to submit the form.

onSubmit() {
}

While the button onclick might look a good place to add the onSubmit listener to, but probably isn’t the right palce. This is because as this is an HTML Form, the default behaviour of the submit type button in a form will get triggered, which will try to submit the form to the service as well as a javsacript submit event.
Angular takes advantage of this event and we can use it to call the onSubmit. This is done by binding to the ngSubmit event on the Angular form.

<form>
  ...
</form>

To get access to the form in the TS, one needs to create a local reference in the HTML file.

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

This would just print out the HTML representation of the form, which is not the way we would want the values.
To get the javacript notation of the form, Angular comes to help.
While create the form in Angular, it gives us a directive to access the form the way Angular sees it. To do that, assing the ngForm to the local form reference.

<form>...</form>
@ViewChild('f') myForm: NgForm;
onSubmit(form: NgForm) {
    console.log(form);
}

This will give the full javascript representation of the form.
One can find all the formcontrols and their values using form.value

TD: Understanding Form State

A lot of attributes can be found in the ngForm isntance and can be used to perform a lot of tasks.
The state is also maintained at the individual controls which can be found in form.controls
Example
control.touched specifies if the control was touched by the user
control.dirty speifies if anything was changed

Accessing the Form with @ViewChild

ViewChild can also be used to get access to the form from TS

<form>...</form>
@ViewChild('f') myForm: NgForm;
onSubmit() {
    console.log(this.myForm);
}
...

This method is useful to get access to the form not only when the button is cliekd, but at any point of time.
Also helpful in validations.</form>

TD: Adding Validation to Check User Input

The form can be modified as:

<input id="email" name="email" required="" type="text" />

required and email are directives that is going to be used by Angular to manage validations.
If one enters something in the email input that is not of tupe email, then the form.valid and form.controls.email.valid would be false
The invalid would also force ng-touched ng-vinvalid … and a couple of classes to the form-input class and can be used for styling of the controls.

Build-in Validators and using HTML5 Validation

Validators can be found at https://angular.io/docs/ts/latest/api/forms/index/Validators-class.html and can also be used in Reactive forms
Directives for Template Driven forms can be found at https://angular.io/api?type=directive
Angular disables native HTML5 validation and can be enabled by adding ngNativeValidate to a control in the template.

TD: Using the Form State

The form states like ng-touched, ng-valid, etc can be used to perform many tasks on the form.
Most importantly, it can be used to disable the Submit button.
To do that just add the disabled property on the button based on the form’s validity

<button disabled="disabled" type="submit">SUBMIT</button>

The classes that get added to the control atuomatically can be used to style them.

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

TD: Outputting Validation Error Messages

To show the user validation errors as warning texts, one can do the following:

<input id="email" name="email" required="" type="text" />

Here also we can pass in the ngModel to the instance of the input and can use it to show/hide other elements in the form.
So we use the input’s valid state to determine the visibility of our validation message.

<span class="help-block">Invalid Email</span>

TD: Set Default Values with ngModel Property Binding

To pass in default values, one can pass the default value to the ngModel directive in the form cotnrol, and pass as one-way attribute binding.

defaultEmail: string = 'someone@email.com';

<input id="email" name="email" required="" type="text" />

TD: Using ngModel with 2-Way Binding

2-Way binding can also be done in the case of NgForm controls.

<input id="email" name="email" required="" type="text" />

This will make sure that the value of defaultEmail is updated on every keystroke in the input field.

TD: Grouping Form Controls

One can also have groups of form-controls with each having their validations. This is easy to implement by adding the ngModelGroup directive to the group holding the form-controls.

<div>
    <input id="username" type="text" />
    <input id="email" name="email" required="" type="text" />
</div>
<input id="firstName" type="text" />

Now the form will have a structure of something like:

form: {
    ...
    value: {
        firstName: 'sometext',
        userDataGroup: {
            username: 'someothedata',
            email: 'some@one.co'
        }
    }
    ...
}

The form.controls will also update to something like the above structure.

TD: Handling Radio Buttons

genders = ['male, 'female']

<div class="radio">
    <label>
        <input name="gender" type="radio" value="" />
        {{ gender }}
    </label>
</div>

TD: Setting and Patching Form Values

There are 2 methods to update the form values from the TS file.
One method is to use the ViewChild way to get access to the form element and use the setValue function of the NgForm

@ViewChild() signupForm: NgForm;

this.signupForm.setValue({
    userData: {
        firstName: 'Name 1',
        secondName: ''
    }
    email: ''
});

The problem with this method is that one has to pass the values for all the controls in the form.
The better way is to use the patchValue method available on the form. In the case of patchValue(), it will only update the controls for whom the information is passed in the form’s javascript object.

this.signupForm.form.patchValue({
    userData: {
        firstName: 'Name 1'
    }
});

The above line will only update the firstName control of the form and every other control will remain as they were earlier.

TD: Using Form Data

onSubmit() {
  this.user.name = this.signupForm.value.userData.firstName + this.signupForm.value.userData.lastName;
  this.user.email = this.signupForm.value.email;
}

TD: Resetting Forms

this.signupForm.reset()

Introduction to the Reactive Approach

Programmatically create the form in TS code.

Reactive: Setup

Need to import ReactiveFormsModule in the app.module imports rather than the FormsModule
Need to declare a var signupForm: FormGroup in the TS file where the FormModule need to be created.

Reactive: Creating a Form in Code

ngOnInit() {
  this.signupForm = new FormGroup({
    'username': new FormControl(null),
    'email': new FormControl(null),
    'gender': new FormControl('male')
  });
}

Lecture 193 – Reactive: Syncing HTML and Form

To synchronize the HTML form with the FormGroup created in TS, we need to pass the formGroup to the HTML Form

<form [formGroup]="formGroup">...</form>

This will inform Angular not to use its own FormGroup but use the FormGroup that we created in the Ts file.
Now a similar pattern is to be used for binding the individual controls. To do this, use the formControlName attribute.

<input id="email" type="text" />

Reactive: Submitting the Form

To submit we can use the local signupForm and no need to pass the form from the HTML. Simply fire a function on ngSubmit function

</form><form>
onSubmit() {
  console.log(this.signupForm);
}

Reactive: Adding Validation

this.signupForm = new FormGroup({
  'username': new FormControl(null, Validators.required), // single validator
  'email': new FormControl(null, [Validators.required, Validators.email]), // multiple validators in array
  'gender': new FormControl('male')
});

Reactive: Getting Access to Controls

To get access to the element, we can use the get method on the Form.

signupForm.get(elementName)

For the overall form, use signupForm.valid

Reactive: Grouping Controls

For creating groups of controls, the TS code can be modified to:

this.signupForm = new FormGroup({
  'userData': new FormGroup({
    'username': new FormControl(null, Validators.required), // single validator
    'email': new FormControl(null, [Validators.required, Validators.email]) // multiple validators in array
  }),
  'gender': new FormControl('male')
});
This should also be reflected in the HTML code, else it would break.
...

<form>
    <div>
        <input name="username" type="text" />
        <input name="email" type="text" />
    </div>
    <input name="gender" type="text" />
</form>

Now to access the controls, one needs to add formGroup path to access,

signupForm.get('userData.username')

Reactive: Arrays of Form Controls (Form Array)

...
<form>...
    <input name="gender" type="text" />
    <div>
        <h4>Your Hobbies</h4>
        <button type="button">Add Hobby</button>
        <div>
            <input type="text" /> // need to do attribute binding as the passed parameter is a variable</div>
        </div>
    </div>
</form><form>
...
this.signupForm = new FormGroup({
    'userData': new FormGroup({
        'username': new FormControl(null, Validators.required), // single validator
        'email': new FormControl(null, [Validators.required, Validators.email]) // multiple validators in array
    }),
    'gender': new FormControl('male'),
    'hobbies': new FormArray([]) // FormControls can be added here in the array also.
});

onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupFrom.get('hobbies')).push(control);
}
...

Reactive: Creating Custom Validators

forbiddenUserNames = ['Chris', 'Anna'];

forbiddenNames(control: FormControl): { [key: string}: boolean } {
    if (this.forbiddenUserNames.indexOf(formControl.value) !== -1) {
        return { 'nameIsForbidden': true };
    }
    return null; // This is for valid entry
}

this.signupForm = new FormGroup({
    'userData': new FormGroup({
        'username': new FormControl(null, [Validators.required, this.forbiddenNames] )
    ...
    })
});

This would not work as the keyword this in the forbiddenNames function isn’t a part of the class that is calling it. So to make sure that it works, one needs to make sure that the class referenCe is passed into the function while calling it. To do this, use:

...
'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)] )
...

Reactive: Using Error Codes

When the validations fails in Angular, these are stored in an errors object in the particular FormControl. It can be fetched using

signupForm.get('userData.username').errors['nameIsForbidden']

Using these we cab show custom error messages.
This name is invalid
This field is required

Reactive: Creating a Custom Async validator

Sometimes validations need to be done on the server side and the client-side needs to wait for the validation to compelte before determining if the form is valid or not.

forbiddenEmails(control: FormControl): Promise | Observable {
    const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        // Forcing a server behavior with time.
        if (control.value === 'test@test.com') {
            resolve({'emailIsForbidden': true});
        } else {
            resolve(null);
        }
        }, 2000);
    });
    return promise;
}

Now this function has to be used in the asyncValidators array in the Form Control

this.signupForm = new FormGroup({
    'userData': new FormGroup({
        'username': new FormControl(null, [Validators.required, this.forbiddenNames] )
        'email': new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails) // the third param is the asyncValidators array
    })
    ...
});

One can also notice ng-pending in the class list of the FormControl when the service is reaching out to the web for validation and then switches to ng-valid/ng-invalid based on result.

Reactive: Reacting to Status or Value Changes

There are two Subsriptions available on the form and its controls tjhat can help track the changes to it.

ngOnInit() {
    ...
    this.signupForm.valueChanges.subscribe((control) => console.log(value));
    this.signupForm.statusChanges.subscribe((status) => console.log(status));
    // This can also be run on the individual controls:
    this.signupForm.get('control-name').valueChanges.subscribe((control) => console.log(value));
    ...
}

Reactive: Setting and Patching Values

To set the complete form

this.signupForm.setValue({
    'userData': {
        'username': 'soeone',
        'email: 'some@some.com'
    }
    'gender': 'male'
});
// To set some spescific part of the form:
this.signupForm.patchValue({
    'userData': {
        'username': 'mur'
    }
});
// To reset the form
this.signupForm.reset()

Adding Validation to the Form

pattern="^[1-9]+[0-9]*$"
// This will make sure that no negative numbers can be entered to the field

Providing a Service correctly

Providing the service into a particular compoenent will make it available as a singleton to that comopnent and its child components, buta s soon as we move away from the component and it is destroyed, the instanc eiof the service also gets destroyed. Thus the next instance created will be a fresh instance. The better way is to provide the service at the module level. In this current case, the aAppModule suits the best.

Leave a Reply