Almost every developer has come to the point where he wants to create his own form components, like a custom text input, a pretty file picker, or just a wrapper component of a library.
To make sure that this custom component works on Template and Reactive forms you will need to implement the ControlValueAccessor
, but some of the times this can be receptive and unnecessary if all you want is to pass the value without changing it.
In this article, I will showcase a way to avoid re-implementing the ControlValueAccessor
but still be able to use the Forms API.
ControlValueAccessor
Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM. - Angular Docs
I will not dive into the ways this interface works, but here is a basic implementation example:
@Component({
standalone: true,
imports: [FormsModule],
selector: 'app-custom-input',
template: `
<input
[ngModel]="value"
(ngModelChange)="onChange($event)"
[disabled]="isDisabled"
(blur)="onTouched()"
/>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: CustomInputComponent,
}],
})
export class CustomInputComponent implements ControlValueAccessor {
value: string;
isDisabled: boolean;
onChange: (value: string) => void;
onTouched: () => void;
writeValue(value: any) {
this.value = value || '';
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this.isDisabled = isDisabled;
}
}
Implementing this is not hard, but it can be lengthy and repetitive. Most of the time we don't even want to change the way the value is bound to the input, so using the angular provided binds would be enough.
Let's look at a way we can stop re-implementing the same logic once and once again.
NgModel under the hood
Let's have a peek at how the ngModel
directive syncs its value with the component.
The directive injects the NG_VALUE_ACCESSOR
to be able to interact with the underlying component, this is the reason why we always need to provide it in our form components.
For the browser native inputs, angular includes some directives that provide NG_VALUE_ACCESSOR
, one of them being the DefaultValueAccessor
.
The ngModel
creates a FormControl
that it uses to keep the state of the control, like value, disabled, touched...
Based on this we can see that our custom component looks like a bridge between the app and the DefaultValueAccessor
, we can also see that there are 2 FormControl
being created in this example, one on each ngModel
.
Taking into account that our custom component is just a bridge and that the 1st ngModel
already has a FormControl
, we can grab this control and just pass it to the input
without modifying it in any way.
Accessing the directive control
The ngModel
, formControl
, and formControlName
are the 3 directives that allow a component to interact with the forms API. From inside a component, we can have access to these directives by injecting the token NgControl
.
So doing this we will have access to the directive that in turn has the FormControl
that we can use.
Here is an example of how this would look like, but for now we still need a dummy value accessor for Angular to see the component as valid for form use.
@Component({
standalone: true,
imports: [ReactiveFormsModule],
selector: 'app-custom-input',
template: `
<input [formControl]="control" />
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: CustomInputComponent,
}],
})
export class CustomInputComponent implements ControlValueAccessor, OnInit {
control: FormControl;
injector = inject(Injector);
ngOnInit() {
// ⬇ It can cause a circular dependency if injected in the constructor
const ngControl = this.injector.get(NgControl, null, { self: true, optional: true });
if (ngControl instanceof NgModel) {
// ⬇ Grab the host control
this.control = ngControl.control;
// ⬇ Makes sure the ngModel is updated
ngControl.control.valueChanges.subscribe((value) => {
if (ngControl.model !== value || ngControl.viewModel !== value) {
ngControl.viewToModelUpdate(value);
}
});
} else {
this.control = new FormControl();
}
}
// ⬇ Dummy ValueAccessor methods
writeValue() { }
registerOnChange() { }
registerOnTouched() { }
}
We need to also make sure the viewToModelUpdate
is called when the value changes, so that the ngModel
is kept updated and that the ngModelChange
is triggered.
FormControl and FormControlName
Let's have a look at how this can the extended to also work with the other directives. The formControl
is the simplest one, all you need to add is this.
if (ngControl instanceof FormControlDirective) {
this.control = ngControl.control;
}
When using formControlName
, to make sure we have the correct fromControl
we need the ControlContainer
, this is what keeps and manages all the controls in a form/group.
So we can inject it and grab the control using the name of the control, like so.
if (ngControl instanceof FormControlName) {
const container = this.injector.get(ControlContainer).control as FormGroup;
this.control = container.controls[ngControl.name] as FormControl;
return;
}
Reuse with Directive Composition
With the 3 directives working, we can look at how to arrange this in a way that is simple to use. I think that the Directive Composition API is a good fit for this.
So if we put all the pieces together in a Directive, this is how it should look like.
@Directive({
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: HostControlDirective,
},
],
})
export class HostControlDirective implements ControlValueAccessor {
control: FormControl;
private injector = inject(Injector);
private subscription?: Subscription;
ngOnInit() {
const ngControl = this.injector.get(NgControl, null, { self: true, optional: true });
if (ngControl instanceof NgModel) {
this.control = ngControl.control;
this.subscription = ngControl.control.valueChanges.subscribe((value) => {
if (ngControl.model !== value || ngControl.viewModel !== value) {
ngControl.viewToModelUpdate(value);
}
});
} else if (ngControl instanceof FormControlDirective) {
this.control = ngControl.control;
} else if (ngControl instanceof FormControlName) {
const container = this.injector.get(ControlContainer).control as FormGroup;
this.control = container.controls[ngControl.name] as FormControl;
} else {
this.control = new FormControl();
}
}
writeValue() { }
registerOnChange() { }
registerOnTouched() { }
ngOnDestroy() {
this.subscription?.unsubscribe();
}
}
And with all the code being in the reusable directive, our custom components looks very clean.
@Component({
standalone: true,
imports: [ReactiveFormsModule],
selector: 'app-custom-input',
template: `
<input [formControl]="hcd.control" />
`,
hostDirectives: [HostControlDirective],
})
export class CustomInputComponent {
hcd = inject(HostControlDirective);
}
Top comments (3)
Amazing, Learned something new!!
From the article, I thought this meant do not "re-implement" Control Value Accessor at all when creating custom input components because Angular has already implemented it for the OOTB value accessors. But what you're really showing here is "re-implement" it once, and reuse this one implementation with NgModel (Template-driven forms), FormControlName directive and FormControlDirective (Reactive forms)! Might want to rename the title of your article because the examples and explanations you've shown here are really insightful for learning how to implement CVA across the board for all Angular Form API options
One question I had was for grabbing the control for FormControlName directive, instead of injecting the parent form ControlContainer class and getting the control via
container.controls[ngControl.name]
, couldn't we just use the same ngControl.control approach you used for FormControlDirective considering that NgControl is provided the same way in both directive classes:FormControlName Provider Config: github.com/angular/angular/blob/89...
FormControlDirective Provider Config: github.com/angular/angular/blob/89...
What about creating a base abstract component that implements CVA as an alternative to not re-implementing CVA more than once? All custom input components can than extend this base abstract component without having to implement CVA themselves. This article shows one way of doing that (although a bit extreme as it has two levels of inheritance): ozak.medium.com/stop-repeating-you.... I haven't seen an example of making this truly reusable with the different form modules but I suspect it would be the same implementation as yours. The only downside I see to this is that composition via directives (your approach) is preferred over inheritance in general for better maintainability.
p.s. Just saw a note in the source code that the ngModelChange output is deprecated as of v6, though it hasn't been removed from the source code yet: github.com/angular/angular/blob/89...
Hey! Thanks for this tip. But how are you going to set writeValue function in different components ?