Greetings!
If you are working in an Angular project, you surely have created many UI components. We can easily use @Input, @Output to communicate data between parent to child and child to parent. However, we are unable to bind those values directly to angular forms. But, do we realy need to do it that way?
If you are working in an Angular project, you surely have created many UI components. We can easily use @Input, @Output to communicate data between parent to child and child to parent. However, we are unable to bind those values directly to angular forms. But, do we realy need to do it that way?
Yes! we need, utilizing the form modules is more powerful way to get
user input. Then, can't we use our 'normal' components to do the job? No! we
can't.
I was ashamed to know that I didn't know the existence of the ControlValueAccessor but better late than never ;)
I was ashamed to know that I didn't know the existence of the ControlValueAccessor but better late than never ;)
Complete code custom-form-control
There are limited amount of html elements available. However, with modern rich user interfaces amount of elements we actually need is unlimited. This is where we use ControlValueAccessor.
This is how we want to use our custom control.
What is ControlValueAccessor?
When you are working with forms in Angular, whether it is template driven or reactive form, NgModel, formControl, formControlName, Angular creates FormControl to track the value, validations etc. Then Angular should have a way to communicate data between actual html element and the FormControl. Angular is smarter enough not to create one to one element mappers, instead it uses ControlValueAccessor.Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM. (documentation)
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
Angular has implemeted this interface for
available elements. For example, for input text, text there is
DefaultValueAccessor
default_value_accessorThere are limited amount of html elements available. However, with modern rich user interfaces amount of elements we actually need is unlimited. This is where we use ControlValueAccessor.
Implement this interface to create a custom form control directive that integrates with Angular forms. (documentation)
writeValue
This method is called by the forms API to write to the view when programmatic changes from model to view are requested. Which means, we use the value passed to this method to update our view.registerOnChange
Registers a callback function that is called when the control's value changes in the UI. This is a callback given to us so that when the value is changed in view, we use this callback to inform form control.registerOnTouched
Registers a callback function that is called by the forms API on initialization to update the form model on blur. That is, we need to use this callback function to inform the form contol about when our view is started to change. This is needed to handle error styling.setDisabledState
Enable, disable the element based on the value passed.Let's create a JIRA like input field
When you open a ticket in JIRA it doesnt show you any edit button. Instead you can click on the text to make it editable. It also gives you save/cancel buttons to choose whether to save or cancel. Let's build this element in Angular! (my css is poor, hence i'll skip styling).View Mode |
Edit Mode |
This is how we want to use our custom control.
<mat-card class="example-input">
<mat-card-content>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<app-view-edit-input formControlName="title" veTitle="Title"></app-view-edit-input>
<app-view-edit-input formControlName="summary" veTitle="Summary"></app-view-edit-input>
</form>
</mat-card-content>
{{ form.value | json }}
</mat-card>
As you can see, we are using formControlName as a normal input element. If you
do like this without implementing ControlValueAccessor, it will give you an
error.
This is our component's template.
As a normal component, we use @Input to get initial value and value for the title.
This is our component's template.
<div class="example-container">
<mat-label class="label">{{ veTitle }}</mat-label>
<mat-form-field appearance="outline" (click)="startEdit()">
<input matInput [formControl]="viewEdit" required autocomplete="off"/>
</mat-form-field>
<ng-container *ngIf="editing">
<div class="buttons">
<span></span>
<button mat-icon-button color="primary" (click)="onSave()">
<mat-icon>done</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="onCancel()">
<mat-icon>cancel</mat-icon>
</button>
</div>
</ng-container>
</div>
It is just normal
template html. We have an input field and two buttons. What I want to do is,
bind this input fields value to parent formControl when save button is
clicked. Reset the value the cancel button is clicked. I also want to accept
values when initializing this (remember, this is default to view mode).
Buttons are visible only when clicked on the input field. (best is to remove
styling in view mode, which my poor css didnt help)Implement the ControlValueAccessor
When we implement the interface methods, we will think that Angular will handle the rest of the work. There is one missing part. Still, Angular doesn't know about our custrom control. Hence we need provide it to existing value accessor list. The way to do is add providers array in component.
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: ViewEditInputComponent
}
]
Note multi: true here as key NG_VALUE_ACCESSOR has multiple
values including Angular's ControlValueAccessor implementations and our custom
controls.As a normal component, we use @Input to get initial value and value for the title.
@Input('value') originalText = '';
@Input('veTitle') veTitle = '';
As registerOnChange and
registerOnTouched give us callback functions, we save it in variables.
_onChange = (value: string) => {};
_onTouched = () => {};
registerOnChange(fn: any): void {
this._onChange = fn;
}
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
When there is a value initially or programatically change the
value, form API will call writeValue with new value. We need to update our
template's input field with this value. We also keep the reference for edit
mode.
writeValue(value: string): void {
this.originalText = value;
this.viewEdit.setValue(this.originalText);
}
With that forms API to custom control synchronization is
completed. Now we want to update forms value with custom controls' value. As
we want to do this only when the save button is clicked, we will call the
callback handler inside save function.
onSave() {
this._onChange(this.originalText);
}
This is the complete code for the custom component.
import { Component, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-view-edit-input',
templateUrl: './view-edit-input.component.html',
styleUrls: ['./view-edit-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: ViewEditInputComponent
}
]
})
export class ViewEditInputComponent implements ControlValueAccessor, OnInit {
@Input('value') originalText = '';
@Input('veTitle') veTitle = '';
viewEdit = new FormControl('');
editing = false;
_onChange = (value: string) => {};
_onTouched = () => {};
disabled = false;
constructor() {
}
ngOnInit(): void {}
writeValue(value: string): void {
this.originalText = value;
this.viewEdit.setValue(this.originalText);
}
registerOnChange(fn: any): void {
this._onChange = fn;
}
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
setDisabledState(disabled: boolean): void {
this.disabled = disabled;
if (disabled) {
this.viewEdit.disable({ onlySelf: true, emitEvent: false });
}
}
startEdit() {
if (!this.disabled) {
this.editing = true;
this._onTouched();
}
}
onSave() {
this.originalText = this.viewEdit.value;
this.editing = false;
this._onChange(this.originalText);
}
onCancel() {
this.viewEdit.setValue(this.originalText);
this.editing = false;
}
}
This is fully Angular form
compatible custom control. We can use validations as well. However, we can
further improve this by adding inbuilt validations for the control itself.References
Happy coding ;)
Comments
Post a Comment