In this post we are going to create a custom component which is designed for Reactive Forms and with a few tweaks can be fully functional for Template Driven Forms within Angular. The component will wrap the Angular Material Form Field to simplify the styling of the component. We will implement the following requirements for this component.
- Password component which can be linked to a form;
- Password visibility to show / hide password in plain text;
- Perform form field validations and display error messages;
- Show as required;
Check out this Stackblitz to see a full working example, and this Github repo for the full code base being built out below.
See the original article on my website: Custom Angular Form Password Component
Initializing the project and component
Step 1: Create Project
ng new angular-custom-password-component --style=scss
Note that the above will set-up the project to use scss stylesheets for the components and the application, if you chose you can leave off the style=scss
to keep the standard css stylesheets.
Step 2: Create Component
ng generate component password-input
Now that we’ve created the project and the base component within the project, let’s start building out the details of the component. We’ll go over the implementation by section to show more of what each part of the code is doing.
Implementing ControlValueAccessor Interface
Step 3: Update Component to implement the Control Value Accessor
import { Component } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
@Component({
selector: 'app-password-input',
templateUrl: './password-input.component.html',
styleUrls: ['./password-input.component.scss']
})
export class PasswordInputComponent implements ControlValueAccessor {
disabled = false;
onChange = (value) => {};
onTouched = () => {};
touched = false;
value: string = null;
constructor() { }
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
writeValue(obj: any): void {
this.value = obj;
}
}
- registerOnChange – registers the callback function within the component when the control’s value is changed within the UI and stores it in the onChange function variable on the component.
- registerOnTouched – registers the callback function which will update the form model on blur and stores it in the onTouched function variable on the component.
- setDisabledState – called by the forms API when changing the status to/from disabled and stores it in the disabled property of the component.
- writeValue – writes a new value to the element and stores it within the value property of the component.
Step 4: Register the component as a Value Access
Most of the components out there will use the NG_VALUE_ACCESSOR
provider which will do some of the auto-wiring for you. However, this component has the need for being able to access the control itself as we’ll see later when getting to the validation portion. To accomplish this, we are going to inject the ngControl
into the constructor. Update the constructor to the following:
constructor(@Optional() @Self() public ngControl: NgControl) {
if (ngControl !== null) {
ngControl.valueAccessor = this;
}
}
The ngControl
gets injected when the component is created by Angular’s Dependency Injection but we need to make sure that we are registering this component as the valueAccessor
. This gives the form API access to the ControlValueAccessor that was implemented.
Step 5: Link the HTML to the component
Let’s start hooking up the work we’ve done to the HTML of the component. As I said in the beginning, this is going to end up being a wrapper around Angular Material. Set the HTML to the following:
<div class="password-input-wrapper">
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput [disabled]="disabled" [value]="value" />
</mat-form-field>
</div>
Now, the value and the disabled attributes are hooked up. So if you initialize a form with a value and a disabled state, then you’ll see that the value is passed down to this component and shows up in the input and/or disables it.
As of now, if you change the value it doesn’t update the parent form. Even though it’s hooked up, it’s only pushing information down from the parent form. We need to implement the two way binding. But first, let’s start building out the parent form to show the functionality in action.
Step 6: Create parent form
<div class="ui-container">
<form [formGroup]="formGroup">
<app-password-input formControlName="password"></app-password-input>
</form>
</div>
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
formGroup: FormGroup = null;
constructor(private _formBuilder: FormBuilder) {
}
ngOnInit() {
this.formGroup = this._formBuilder.group({
password: this._formBuilder.control(null)
});
}
}
Here it’s a very basic form, with just the initialization of the password component with a null value and setting the form control name to link the component. An issue with the way the form is currently set-up is that you can’t see anything happen. So let’s update the HTML to following:
<div class="ui-container">
<form [formGroup]="formGroup">
<app-password-input formControlName="password"></app-password-input>
</form>
<div>
<span>Form values</span>
<pre>{{ formGroup.value | json}}</pre>
</div>
</div>
Step 7: Listening for changes
First, enter the listener into the password component.
onInput($event: any): void {
this.value = $event.currentTarget.value;
this.onChange(this.value);
}
Then hook it up to the HTML with the input event binding.
<input matInput [disabled]="disabled" [value]="value" (input)="onInput($event)" />
Now, you can see updates in the component are passed into the parent form and available to be used.
Implementing Validations
At this point, you have a functional component which you can hook up to a Reactive Form. Depending on your needs, this may be enough but from my experience developing enterprise level components we need to at least implement validations. In order to do that, we have a couple more things to wire up. The first being the onTouched
event. The material component won’t show any mat-errors nor will it highlight the field as invalid unless the component has been touched.
Step 8: Register onTouched events
Technically, we registered the onTouch
event earlier into this post. However, it’s just registered, we aren’t actually using it. It’s pretty simple to wire up, just add the event that you want to trigger it such as blur or focus out. In this case, we are using focus out.
<input matInput [disabled]="disabled" [value]="value" (input)="onInput($event)" (focusout)="onFocusOut()" />
Then the corresponding method on the component.
onFocusOut(): void {
this.onTouched();
}
Now it’s time to diverge from the normal a little bit, and while I was building out this component for my own application and this posting, there were still a few things that my component wasn’t doing which I wanted it to do.
- Mark the field with asterisks when providing the required validator in the parent form;
- Mark the field red when it’s invalid;
- Show mat-error messages;
As I mentioned earlier, I had injected the ngControl
because of an issue I encountered with validations. It was marking the field with the asterisks. After doing some digging in the mat-input
/ mat-form-field
components from angular I discovered that I could access the control and check to see if it had the required validator associated with it. I do this through a getter and setter of the required attribute, this way it supports template-driven design and reactive-forms. The template-driven comes from the input decorator itself which will store and override the missing validator. Then for reactive-forms I tap into the control and check if the validator exists.
get required(): boolean {
return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
}
@Input()
set required(value: boolean) {
this._required = value;
}
And then link it up with the HTML.
<input matInput [disabled]="disabled" [value]="value" (input)="onInput($event)" (focusout)="onFocusOut()" [required]="required" />
In order to meet the last two aspects of my requirement, I had to implement an errorStateMatcher
in addition with notifying the mat-input
to update its error state.
Step 9: Register Error State Matcher
Update the component so that it implements the ErrorStateMatcher by adding the interface to the implements collection.
export class PasswordInputComponent implements ControlValueAccessor, ErrorStateMatcher {
}
Then implement the interface by implementing the isErrorState
method.
isErrorState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean {
return this.touched && (this.ngControl?.control?.invalid ?? false);
}
Following along with standard mat-form-field
implementations, we are going to make sure that the field has been touched and then, again, access the control itself on the ngControl
to make sure it is invalid.
Next update the HTML to register it with the input control.
<input matInput
[disabled]="disabled"
[errorStateMatcher]="matcher"
(focusout)="onFocusOut()"
(input)="onInput($event)"
[required]="required"
[value]="value"
/>
Step 10: Notify MatInput for Error State Changes
The final piece to getting the validations and mat-errors
to show up within custom control component, as if they would with an implementation directly associated to the form. We need to tell mat-input to update its error state, but first we need to be able to access it. We’ll do this using the @ViewChild
decorator to put it into the component.
@ViewChild(MatInput)
matInput: MatInput;
Then, depending on how quickly you want the error state to be updated you can add the call to the onInput method. I chose to do it on the focusout
call to make it react more closely with angular material.
onFocusOut(): void {
this.onTouched();
this.matInput.updateErrorState();
}
The last and final piece would be to add the mat-errors to the HTML component. Unfortunately, I tried many different ways to inject the messages from the parent down into the component but was unable to find an acceptable solution. So adding errors such as this will allow them to show when the control has the validation message.
<mat-error *ngIf="ngControl.hasError('required')">Password is a required field.</mat-error>
Enhanced Features
Step 11: Password Visibility Toggle
It’s pretty standard now, that on a password field you have the option to toggle the password formatting of the input into plain text. So let’s add one to our component.
In the HTML add the icon we’ll use as the toggle.
<mat-icon matSuffix (click)="onVisibilityClick($event)">{{ icon }}</mat-icon>
The onVisibilityClick
implementation:
onVisibilityClick($event): void {
if (this._visible) {
this.icon = 'visibility_off';
this.type = 'password';
} else {
this.icon = 'visibility';
this.type = 'text';
}
// Invert the value.
this._visible = !this._visible;
$event.stopPropagation();
}
We need to make sure that we are toggling the icon which will be used as feedback to the user to indicate which mode the input is in. We also need to change the type of the input to convert it from a password input to plain text and vice versa.
One thing that I noticed while implementing the toggle, (especially with the floating label from Angular Material) is that when you click on the toggle the label will jump around as the input regains focus after the click event propagates up the chain. To resolve that I passed in the $event object and called the stopPropagation
method to prevent the bubbling up of the click event.
Step 12: Dynamic label
Unless you want to call every field password every time you want to use this component, you’ll want to make sure that you can provide a label from any parent component.
Update the HTML to:
<mat-label>{{ label }}</mat-label>
Add the input to the component so it can be declared:
@Input()
label: string = null;
Step 13: Adding error validations
The final portion of the component is displaying validation errors underneath the field when there are validation messages within the form. We are going to hard code a specific message for the required error to enhance the earlier feature we implemented. We are also going to allow for a custom input of an error message and the name of the corresponding control. This way, in the parent component you are able to provide custom validators and then have the message displayed as an error.
<mat-error *ngIf="ngControl.hasError('required')">{{ label }} is a required field.</mat-error>
<mat-error *ngIf="ngControl.hasError(customErrorName)">{{ customErrorMessage }}</mat-error>
We are re-using the dynamic label within the required message to link the elements together and we are checking to see for the custom error. Here again, you can see how we are using the ngControl
that was injected earlier.
Don’t forget to define the inputs for the custom error message.
@Input()
customErrorMessage: string = null;
@Input()
customErrorName: string = null;
And that's it. You now have a custom Password component which can be used in reactive forms.
Using the Component
The component itself is pretty easy to use once it’s set-up. You just need to set-up your form group, link the controls to the component and provide any custom error messages you may want. As I mentioned earlier in this article, I’m display the errors and the values of form to be able to see the changes.
The HTML of the parent form:
<div class="ui-container">
<div class="ui-input-container">
<form [formGroup]="formGroup">
<div>
<app-password-input
formControlName="password"
label="Password"
customErrorName="passwordStrength"
[customErrorMessage]="invalidPasswordMessage"></app-password-input>
</div>
<div>
<app-password-input
formControlName="confirm"
label="Confirm Password"
customErrorName="passwordMismatch"
[customErrorMessage]="confirmPasswordMessage"></app-password-input>
</div>
</form>
</div>
<div>
<span>Form values</span>
<pre>{{ formGroup.value | json}}</pre>
</div>
<div>
<span>Form Errors</span>
<pre>{{ formGroup.get('password').errors | json }}</pre>
</div>
</div>
And the parent component:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validator, Validators } from '@angular/forms';
import { passwordStrengthValidator } from './validators/password-strength-validator';
import { confirmPasswordValidator } from './validators/password-match-validator';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
formGroup: FormGroup = null;
confirmPasswordMessage = 'The passwords do not match.';
invalidPasswordMessage = 'Must contain at least 1 number, 1 uppercase letter, 1 lowercase letter and at least 8 characters.';
constructor(private _formBuilder: FormBuilder) {
}
ngOnInit() {
const passwordControl = this._formBuilder.control({
disabled: false,
value: null
}, [Validators.required, Validators.minLength(8), passwordStrengthValidator()]);
const confirmPasswordControl = this._formBuilder.control({
disabled: false,
value: null
}, [Validators.required, Validators.minLength(8), confirmPasswordValidator(passwordControl)]);
this.formGroup = this._formBuilder.group({
confirm: confirmPasswordControl,
password: passwordControl
});
}
}
Thanks for taking the time to read the article and I hope that it helped you out.
Just as a reminder, you can see a full working example Stackblitz and the code itself in Github.
Top comments (0)