29
loading...
This website collects cookies to deliver better user experience
ControlValueAccessor
it and how to use it to create really cool form.ngModel
(Template Driven Forms) or formControl
(Reactive Forms).@Input()
and @Output()
to receive and send form values to the parent form component. I used to listen to the changes in the child component and then emit the values to the parent.interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
writeValue()
- this function is called by the Forms API to update the value of the element. When ngModel
or formControl
value changes, this function gets called and the latest value is passed in as the argument to the function. We can use the latest value and make changes in the component. (ref)registerOnChange()
- we get access to a function in the argument that can be saved to a local variable. Then this function can be called when there are any changes in the value of our custom form control. (ref)registerOnTouched()
- we get access to another function that can be used to update the state of the form to touched
. So when the user interacts with our custom form element, we can call the saved function to let Angular know that the element has been interacted with. (ref)setDisabledState()
- this function will be called by the forms API when the disabled state is changed. We can get the current state and update the state of the custom form control. (ref)NG_VALUE_ACCESSOR
token in the component's providers array like so:const COUNTRY_CONTROL_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomFormControlComponent),
multi: true,
};
@Component({
selector: 'app-country-selector',
template: ``,
providers: [COUNTRY_CONTROL_VALUE_ACCESSOR], // <-- provided here
})
export class CustomFormControlComponent implements ControlValueAccessor {}
providers
. Also you can see the use of forwardRef
(ref) here. It is needed because we are referring to the CountrySelectorComponent
class which is not defined before its reference.{
name: 'Adithya',
github: 'https://github.com/AdiSreyaj',
website: 'https://adi.so',
server: 'IN',
communications: [{
label: 'Marketing',
modes: [{
name: 'Email',
enabled: true,
},
{
name: 'SMS',
enabled: false,
}],
},
{
label: 'Product Updates',
modes: [{
name: 'Email',
enabled: true,
},
{
name: 'SMS',
enabled: true,
}],
},
]
}
server
and the communications
fields are going to be connected to a custom form control. We are using Reactive Forms in the example.const form = this.fb.group({
name: [''],
github: [''],
website: [''],
server: [''],
communications: [[]]
});
<form [formGroup]="form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" formControlName="name">
</div>
<div class="form-group">
<label for="github">Github</label>
<input type="url" id="github" formControlName="github">
</div>
<div class="form-group">
<label for="website">Website</label>
<input type="url" id="website" formControlName="website">
</div>
<div class="form-group">
<label>Region</label>
<app-country-selector formControlName="server"></app-country-selector>
</div>
<div class="form-group">
<label>Communication</label>
<app-communication-preference formControlName="communications"></app-communication-preference>
</div>
</form>
formControlName
on the app-country-selector
and app-communication-preference
components. This will be only possible if those components are implementing the ControlValueAccessor
interface. This is how you make a component behave like a form control.const COUNTRY_CONTROL_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CountrySelectorComponent),
multi: true,
};
@Component
decorator.@Component({
selector: 'app-country-selector',
template: `
<div>
<ng-container *ngFor="let country of countries">
<button [disabled]="disabled" (click)="selectCountry(country.code)"
[class.selected]="!disabled && selected === country.code">
<ng-container *ngIf="!disabled && selected === country.code">
<!-- Checkmark Icon -->
</ng-container>
<img [src]="...flag src" [alt]="country.name" />
<p>{{ country?.name }}</p>
</button>
</ng-container>
</div>
`,
providers: [COUNTRY_CONTROL_VALUE_ACCESSOR],
})
export class CountrySelectorComponent implements ControlValueAccessor {
countries = [
{ code: 'IN', name: 'India' },
{ code: 'US', name: 'United States' },
{ code: 'GB-ENG', name: 'England' },
{ code: 'NL', name: 'Netherlands' },
];
selected!: string;
disabled = false;
private onTouched!: Function;
private onChanged!: Function;
selectCountry(code: string) {
this.onTouched(); // <-- mark as touched
this.selected = code;
this.onChanged(code); // <-- call function to let know of a change
}
writeValue(value: string): void {
this.selected = value ?? 'IN';
}
registerOnChange(fn: any): void {
this.onChanged = fn; // <-- save the function
}
registerOnTouched(fn: any): void {
this.onTouched = fn; // <-- save the function
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
}
server
in the form, we will get the initial value in the writeValue()
method. We get the value and assign it to our local variable selected
which manages the state.touched
and then assign the value to the selected
variable. The main part is we also call the onChanged
method and pass the newly selected country code. This will set the new value as the form control's value.setDisabledState()
method we can implement the disabled state for our component. So If we trigger disable from the form using:this.form.get('server').disable();
setDisabledState()
method where the state isDisabled
is passed, which is then assigned to a local variable disabled
. Now we can use this local variable to add a class or disable the button.setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
const COM_PREFERENCE_CONTROL_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CommunicationPreferenceComponent),
multi: true,
};
@Component({
selector: 'app-communication-preference',
template: `<div>
<ul>
<ng-container *ngFor="let item of options; index as i">
<li>
<p>{{ item?.label }}</p>
<div>
<ng-container *ngFor="let mode of item.modes; index as j">
<div>
<input
type="checkbox"
[id]="item.label + mode.name"
[(ngModel)]="mode.enabled"
(ngModelChange)="handleChange(i, j, $event)" />
<label [for]="item.label + mode.name">{{ mode.name }}</label>
</div>
</ng-container>
</div>
</li>
</ng-container>
</ul>
</div>`,
providers: [COM_PREFERENCE_CONTROL_VALUE_ACCESSOR],
})
export class CommunicationPreferenceComponent implements ControlValueAccessor {
options: CommunicationPreference[] = [];
private onTouched!: Function;
private onChanged!: Function;
handleChange(itemIndex: number, modeIndex: number, change: any) {
this.onTouched();
this.options[itemIndex].modes[modeIndex].enabled = change;
this.onChanged(this.options);
}
writeValue(value: any): void {
this.options = value;
}
registerOnChange(fn: any): void {
this.onChanged = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}
options
variable that manages the local state of the component. When there is any value-change triggered by the form, we get the new value in the writeValue
method, we update the local state with the changed value.onChanged
method and pass the updated state which updates the form as well.ControlValueAccessor
. By implementing few methods, we can directly hook our component to a Reactive
or Template Driven
form with ease.