Can I access to formControl of my custom ControlValueAccessor in Angular 2+?
Here is a sample showing how to get (and re-use) the underlying FormControl and the underlying ControlValueAccessor.
This is useful when wrapping a component (like an input) since you can just re-use the already existing FormControl and ControlValueAccessor that angular creates which lets you avoid having to re-implement it.
@Component({
selector: 'resettable-input',
template: `
<input type="text" [formControl]="control">
<button (click)="clearInput()">clear</button>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: ResettableInputComponent,
multi: true
}]
})
export class ResettableInputComponent implements ControlValueAccessor {
@ViewChild(FormControlDirective, {static: true}) formControlDirective: FormControlDirective;
@Input() formControl: FormControl;
@Input() formControlName: string;
// get hold of FormControl instance no matter formControl or formControlName is given.
// If formControlName is given, then this.controlContainer.control is the parent FormGroup (or FormArray) instance.
get control() {
return this.formControl || this.controlContainer.control.get(this.formControlName);
}
constructor(private controlContainer: ControlContainer) { }
clearInput() {
this.control.setValue('');
}
registerOnTouched(fn: any): void {
this.formControlDirective.valueAccessor.registerOnTouched(fn);
}
registerOnChange(fn: any): void {
this.formControlDirective.valueAccessor.registerOnChange(fn);
}
writeValue(obj: any): void {
this.formControlDirective.valueAccessor.writeValue(obj);
}
setDisabledState(isDisabled: boolean): void {
this.formControlDirective.valueAccessor.setDisabledState(isDisabled);
}
}
SAMPLE PLUNKER
I see two options:
- Propagate the errors from component
FormControl
to<select>
FormControl
whenever the<select>
FormControl
value changes - Propagate the validators from component
FormControl
to<select>
FormControl
Below the following variables are available:
selectModel
is theNgModel
of the<select>
formControl
is theFormControl
of the component received as an argument
Option 1: propagate errors
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
Option 2: propagate validators
ngAfterViewInit(): void {
this.selectModel.control.setValidators(this.formControl.validator);
this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
}
The difference between the two is that propagating the errors means having already the errors, while the seconds option involves executing the validators a second time. Some of them, like async validators might be too costly to perform.
Propagating all properties?
There is no general solution to propagate all the properties. Various properties are set by various directives, or other means, thus having different lifecycle, which means that require particular handling. Current solution regards propagating validation errors and validators. There are many properties available up there.
Note that you might get different status changes from the FormControl
instance by subscribing to FormControl.statusChanges()
. This way you can get whether the the control is VALID
, INVALID
, DISABLED
or PENDING
(async validation is still running).
How validation works under the hood?
Under the hood the validators are applied using directives (check the source code). The directives have providers: [REQUIRED_VALIDATOR]
which means that own hierarchical injector is used to register that validator instance. So depending on the attributes applied on the element, the directives will add validator instances on the injector associated to the target element.
Next, these validators are retrieved by NgModel
and FormControlDirective
.
Validators as well as value accessors are retrieved like:
constructor(@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
and respectively:
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[])
Note that @Self()
is used, therefore own injector (of the element to which the directive is being applied) is used in order to obtain the dependencies.
NgModel
and FormControlDirective
have an instance of FormControl
which actually update the value and execute the validators.
Therefore the main point to interact with is the FormControl
instance.
Also all validators or value accessors are registered in the injector of the element to which they are applied. This means that the parent should not access that injector. So would be a bad practice to access from current component the injector provided by the <select>
.
Sample code for Option 1 (easily replaceable by Option 2)
The following sample has two validators: one which is required and another which is a pattern which forces the option to match "option 3".
The PLUNKER
options.component.ts
import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import {SettingsService} from '../settings.service';
const OPTIONS_VALUE_ACCESSOR: any = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OptionsComponent)
};
@Component({
providers: [OPTIONS_VALUE_ACCESSOR],
selector: 'inf-select[name]',
templateUrl: './options.component.html',
styleUrls: ['./options.component.scss']
})
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit {
@ViewChild('selectModel') selectModel: NgModel;
@Input() formControl: FormControl;
@Input() name: string;
@Input() disabled = false;
private propagateChange: Function;
private onTouched: Function;
private settingsService: SettingsService;
selectedValue: any;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
ngOnInit(): void {
if (!this.name) {
throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
}
}
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
writeValue(obj: any): void {
this.selectedValue = obj;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
options.component.html
<select #selectModel="ngModel"
class="form-control"
[disabled]="disabled"
[(ngModel)]="selectedValue"
(ngModelChange)="propagateChange($event)">
<option value="">Select an option</option>
<option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
{{option.description}}
</option>
</select>
options.component.scss
:host {
display: inline-block;
border: 5px solid transparent;
&.ng-invalid {
border-color: purple;
}
select {
border: 5px solid transparent;
&.ng-invalid {
border-color: red;
}
}
}
Usage
Define the FormControl
instance:
export class AppComponent implements OnInit {
public control: FormControl;
constructor() {
this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required]));
}
...
Bind the FormControl
instance to the component:
<inf-select name="myName" [formControl]="control"></inf-select>
Dummy SettingsService
/**
* TODO remove this class, added just to make injection work
*/
export class SettingsService {
public getOption(name: string): [{ description: string }] {
return [
{ description: 'option 1' },
{ description: 'option 2' },
{ description: 'option 3' },
{ description: 'option 4' },
{ description: 'option 5' },
];
}
}
Here is what in my opinion is the cleanest solution to access FormControl in a ControlValueAccessor
based component. Solution was based on what is mention here in Angular Material documentation.
// parent component template
<my-text-input formControlName="name"></my-text-input>
@Component({
selector: 'my-text-input',
template: '<input
type="text"
[value]="value"
/>',
})
export class MyComponent implements AfterViewInit, ControlValueAccessor {
// Here is missing standard stuff to implement ControlValueAccessor interface
constructor(@Optional() @Self() public ngControl: NgControl) {
if (ngControl != null) {
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
ngControl.valueAccessor = this;
}
}
ngAfterContentInit(): void {
const control = this.ngControl && this.ngControl.control;
if (control) {
// FormControl should be available here
}
}
}