Revolutionize Your Angular Forms: Mastering Custom Inputs with and without Reactive Form Control
Table of contents
Custom inputs can be a powerful tool in Angular applications, allowing you to create unique user interfaces and tailor the user experience to your specific needs. With the @angular/forms
package, you can create custom inputs that work seamlessly with the built-in form control functionality in Angular. In this guide, we will walk you through the process of creating custom inputs with and without form control, providing you with the knowledge you need to take your Angular applications to the next level. We'll cover everything from the basics of creating custom inputs to more advanced techniques for creating complex interfaces that are easy to use and maintain. By the end of this guide, you'll have a solid foundation in custom input development in Angular and be able to create custom inputs that meet your specific needs.
Reactive forms in Angular offer a more robust way to build complex and dynamic forms. It uses the power of observables to manage the state of form data and track changes in real time. In this post, we will take a look at how to build a reactive form with input fields in Angular.
Using ReactiveFormsModule to build a basic reactive form can be done by:
import { FormGroup, FormControl, Validators } from '@angular/forms';
In the FormComponent
class, create a new form group by adding the following code:
formGroup: FormGroup;
Then, in the constructor()
method, create a new form group, and add form controls to it as follows:
constructor(private formBuilder: FormBuilder) {
this.formGroup = this.formBuilder.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
}
In the above code, we are creating a form group with four form controls: name
and email
. We have also added some validators to each control to ensure that the user enters valid data.
Next, let's bind our form controls to the input fields in the template. Open the form.component.html
file and add the following code:
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<label>
Name:
<input type="text" formControlName="name">
</label>
<label>
Email:
<input type="email" formControlName="email">
</label>
<button type="submit" [disabled]="!formGroup.valid">Submit</button>
</form>
Create the onSubmit()
method to handle the form submission.
onSubmit() {
if (this.formGroup.valid) {
// Handle form submission
console.log('Form submitted successfully');
console.log(this.formGroup.value);
} else {
// Display error message
console.log('Form validation failed');
}
}
We would be creating a tailwind css enabled Input
with mat-icon
prefix and suffix when the input is of type password.
<label>
<p
class="font-medium text-primaryText pb-1 text-sm {{labelClass }}"
>
{{ label }}
</p>
<div class="relative {{ wrapperClass }}">
<div
class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"
*ngIf="icon">
<mat-icon class="w-5 h-5 text-gray-500">{{ icon }}</mat-icon>
</div>
<input
name="{{ name }}"
type="{{
type == 'text' ? type : showPassword ? 'text' :'password'
}}"
(input)="
onChange($any($event.target).value);getChangeEvent($event)
"
(blur)="onTouched(); blur.emit()"
[disabled]="disabled"
[placeholder]="placeholder"
class="{{
inputClass
}} w-full py-3 border placeholder:text-sm text-primaryText border-border rounded-lg focus:outline-none focus:border-slate-500 hover:shadow"
[ngClass]="{
'!border-critical !border':
formControls?.touched && !formControls?.valid,
'pl-10 pr-3': icon,
'pl-3 pr-10': type == 'password',
'px-3': !icon
}"
[value]="formControls?.value || value"
/>
<div
class="absolute inset-y-0 right-0 flex items-center pr-3"
*ngIf="type == 'password'" >
<mat-icon
(click)="togglePasswordVisibility()"
class="w-5 h-5 cursor-pointer text-primaryText"
>
{{
!showPassword ? "visibility_off" : "visibility"
}}
</mat-icon>
</div>
</div>
</label>
<div
*ngIf="formControls?.invalid && formControls?.touched"
class="text-critical mt-1 italic text-xs"
>
This field is required!
</div>
To enable the easy customization of the Input
we have extended come class names that can be passed in as props from the component where it would be used.
label
: This binding is used to display the label text for the input field. It is of type string and is passed as an input to the component.name
: This binding is used to set thename
attribute of the input field. It is of type string and is passed as an input to the component.placeholder
: This binding is used to set the placeholder text for the input field. It is of type string and is passed as an input to the component.type
: This binding is used to set the type of the input field. It is of type string and is passed as an input to the component. The default value ispassword
but it can be set totext
to display a regular text input field.icon
: This binding is used to display an icon on the left-hand side of the input field. It is of type string and is passed as an input to the component.value
: This binding is used to set the initial value of the input field. It is of type string and is passed as an input to the component.disabled
: This binding is used to disable the input field. It is of type boolean and is passed as an input to the component.wrapperClass
: This binding is used to set the class for the input field wrapper. It is of type string and is passed as an input to the component.inputClass
: This binding is used to set the class for the input field. It is of type string and is passed as an input to the component.labelClass
: This binding is used to set the class for the label of the input field. It is of type string and is passed as an input to the component.onChange
: This binding is used to emit an event when the value of the input field changes. It is of type EventEmitter and is passed as an output from the component.onTouched
: This binding is used to emit an event when the input field loses focus. It is of type EventEmitter and is passed as an output from the component.blur
: This binding is used to emit an event when the input field loses focus. It is of type EventEmitter and is passed as an output from the component.formControls
: This binding is used to access the form control for the input field. It is of type AbstractControl and is passed as an input to the component.
First, we create out togglePasswordVisibility
function in the component.ts
showPassword: boolean = false;
togglePasswordVisibility() {
this.showPassword = !this.showPassword;
}
Then we proceed to the constructor.
Self
: This decorator tells Angular to look for dependencies only in the local injector.Optional
: This decorator indicates that the dependency is optional.
constructor(@Self() @Optional() public controlDir: NgControl) {
if (this.controlDir) {
controlDir.valueAccessor = this;
}
}
Constructor:
The constructor of the InputComponent
class takes an optional NgControl
argument. The @Self()
decorator tells Angular to look for the NgControl
dependency only in the local injector, and the @Optional()
decorator indicates that the dependency is optional.
In the constructor, if the NgControl
argument is provided, the component's value accessor is set to this instance of the InputComponent
. The value accessor is responsible for reading and writing the value of the input field.
Then we proceed to mount the component after injecting the dependencies.
ngOnInit(): void {
if (!this.controlDir) {
this.writeValue(this.value);
} else {
const control = this.controlDir.control;
const validators = control?.validator
? [control.validator, Validators.required]
: Validators.required;
control?.setValidators(validators);
}
}
writeValue(value: any): void {
if (this.controlDir) {
value &&
this.controlDir.control?.setValue(value, { emitEvent: false });
}
}
Lifecycle hooks:
The ngOnInit()
method is a lifecycle hook that is called after the component's inputs have been initialized. In this method, the writeValue()
method is called if there is no NgControl
directive, and the Validators.required
validator is added if there is a NgControl
directive.
The writeValue()
method is responsible for writing the initial value of the input field. If there is an NgControl
directive.
get formControls() {
if (!this.controlDir) {
return null;
}
return this.controlDir.control;
}
The get formControls
returns the form control.
onChange: (value: any) => void = () => {};
onTouched: () => void = () => {};
onChange
and onTouched
are callback functions that are used to interact with Angular's form control directives, specifically when creating custom form controls that integrate with Angular's FormControl
API.
onChange
is a function that will be called whenever the value of the custom form control changes. It takes in a parametervalue
of typeany
, which represents the new value of the control. If no value is passed in, the default value of an empty function is assigned using the syntax= () => {}
.onTouched
is a function that will be called when the custom form control is blurred, indicating that the user has interacted with the control and then moved away from it. It takes in no parameters and, likeonChange
, has a default value of an empty function if no value is passed in.
By setting these functions as class properties, they can be used to interact with Angular's form control directives and provide custom behavior for the custom form control. For example, when the user interacts with the custom form control, the onChange
function can be called to update the value of the control, and when the control is blurred, the onTouched
function can be called to update the control's touched state.
registerOnChange(onChange: (value: any) => void): void {
this.onChange = onChange;
}
registerOnTouched(onTouched: () => void): void {
this.onTouched = onTouched;
}
These are two methods required by Angular's ControlValueAccessor interface which is used to create a custom form control that can be used with Angular's reactive forms.
The registerOnChange
method registers a callback function that should be called by the custom form control whenever its value changes. The callback function takes a single argument, which is the new value of the custom form control. In this component, the onChange
method is set to the function passed in as an argument to registerOnChange
. Whenever the custom form control's value changes, this method will be called with the new value.
The registerOnTouched
method registers a callback function that should be called by the custom form control whenever it is touched by the user. In this component, the onTouched
method is set to the function passed in as an argument to registerOnTouched
. Whenever the user interacts with the custom form control, this method will be called to signal that the control has been touched.
These methods are necessary for the custom form control to communicate with Angular's reactive forms, and are used to update the model value and the control's validity status.
Conclusion:
Our component.ts
should look like this.
import {
Component,
EventEmitter,
Input,
Output,
Self,
Optional,
} from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
@Component({
selector: "app-input",
templateUrl: "./input.component.html",
styleUrls: ["./input.component.scss"],
providers: [],
})
export class InputComponent {
@Input() placeholder: string = "";
@Input() wrapperClass: string = "";
@Input() inputClass: string = "";
@Input() labelClass: string = "";
@Input() icon: string | undefined;
@Input() name: string = "";
@Input() type: string = "text";
@Input() label: string = "";
@Input() value: string = "";
@Output() blur: EventEmitter<void> = new EventEmitter<void>();
@Output() onChangeEvent: EventEmitter<string> = new EventEmitter<string>();
@Input() disabled!: boolean;
showPassword: boolean = false;
onChange: (value: any) => void = () => {};
onTouched: () => void = () => {};
constructor(@Self() @Optional() public controlDir: NgControl) {
if (this.controlDir) {
controlDir.valueAccessor = this;
}
}
ngOnInit(): void {
if (!this.controlDir) {
this.writeValue(this.value);
} else {
const control = this.controlDir.control;
const validators = control?.validator
? [control.validator, Validators.required]
: Validators.required;
control?.setValidators(validators);
}
}
writeValue(value: any): void {
if (this.controlDir) {
value &&
this.controlDir.control?.setValue(value, { emitEvent: false });
}
}
registerOnChange(onChange: (value: any) => void): void {
this.onChange = onChange;
}
registerOnTouched(onTouched: () => void): void {
this.onTouched = onTouched;
}
getChangeEvent(event: Event) {
let value = (event.target as HTMLInputElement).value;
this.onChangeEvent.emit(value);
}
togglePasswordVisibility() {
this.showPassword = !this.showPassword;
}
get formControls() {
if (!this.controlDir) {
return null;
}
return this.controlDir.control;
}
}
Usage:
<form [formGroup]="form" (submit)="onSubmit()" novalidate>
<div>
<app-input
formControlName="email"
placeholder="e.g example@mail.com"
label="Email address"
></app-input>
</div>
<div>
<app-input
formControlName="password"
placeholder="*********"
label="Enter password"
[type]="'password'"
></app-input>
</div>
<button>Submit</button>
</form>
Or without form control like this
<app-input
placeholder="e.g mail.com"
label="Enter mail"
icon="mail"
(onChangeEvent)="getEvent($event)"
></app-input>