DEV Community

Lloyd Software Solutions
Lloyd Software Solutions

Posted on

Custom Angular Form Password Component

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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)
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Then hook it up to the HTML with the input event binding.

<input matInput [disabled]="disabled" [value]="value" (input)="onInput($event)" />
Enter fullscreen mode Exit fullscreen mode

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()" />
Enter fullscreen mode Exit fullscreen mode

Then the corresponding method on the component.

onFocusOut(): void {
    this.onTouched();
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

And then link it up with the HTML.

<input matInput [disabled]="disabled" [value]="value" (input)="onInput($event)" (focusout)="onFocusOut()" [required]="required" />
Enter fullscreen mode Exit fullscreen mode

Image description

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 {
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Add the input to the component so it can be declared:

@Input()
label: string = null;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)