Reputation: 985
I have an input component customInput
that creates a classic input field and adds some layouting-spice to it, no additional logic.
I want to pass a formControl to it, to bind it to the input it contains.
Should be used like this:
<form [formGroup]="form">
<custom-input [formControl]="form.controls['control']"></custom-input>
</form>
Inside Custom Input:
export class HidInputFieldComponent {
@Input() formControl: AbstractControl
...
}
<div class="container">
<input [formControl]="formControl"/>
<label>label</label>
</div>
Now when i initialize the component, i get
No value accessor for form control with unspecified name
Logging the control in my components constructor, it is undefined.
Am I doing it wrong or isn't there a way around ControlValueAccessor
? Since I am not actually building a custom control (I still use classic input) it seems extreme
Upvotes: 25
Views: 37875
Reputation: 107
I know I'm way late to the party, but I stumbled upon this question with the same issue so I hope this helps someone on the future.
In my case, renaming @Input() formControl: FormControl
to @Input() control: FormControl
fixed the error.
Upvotes: 7
Reputation: 605
Create yourself a custom getCtrl function in the TS file:
getCtrl(name: string): FormControl {
const ctrl = this.form.get(name) as FormControl;
if (!ctrl) throw 'Missing Form Control for ' + name;
return ctrl;
}
and then call it as many times as you want in the template:
<div>
<app-form-control
[control]="getCtrl('language')"
></app-form-control>
</div>
Upvotes: 1
Reputation: 459
You don't need to import ControlValueAccessor or anything similar to accomplish that.
All you need to do is to pass the FormControl object to your child component like this:
<form [formGroup]="form">
<custom-input [control]="form.controls['theControlName']">
</custom-input>
</form>
That means your custom-input component should look like this:
import {Component, Input} from '@angular/core';
import {FormControl} from '@angular/forms';
@Component({
selector: 'custom-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss']
})
export class InputComponent {
@Input() control: FormControl;
}
And the template:
<input [formControl]="control">
And that's it.
If you implement the custom input like that you won't have to bring the parent formGroup into the child component logic, it is completely unnecessary there (Unless you need to make some operations on it or on some of the rest of form controls).
Besides, passing the FormControl object to the custom input would give you access to the properties of it without referencing the FormGroup and then getting the specific control, because that's a work done on the parent component.
I hope this solution helps to simplify the work of many people as it's pretty common to make this kind of custom controls.
Upvotes: 37
Reputation: 1888
this answer from Johann Garrido is good, however it introduces extra input [control]
that you'll need to always keep in mind when working with custom components.
Plus, it tied to work directly to ReactiveFormsModule
because it only accepts FormControl
instance.
Better way to do this is in my opinion is to implement ControlValueAccessor
interface, but utilize some workaround to not duplicate control handling:
export const NOOP_VALUE_ACCESSOR: ControlValueAccessor = {
writeValue(): void {},
registerOnChange(): void {},
registerOnTouched(): void {}
};
And use NOOP_VALUE_ACCESSOR
in component that wraps the form control:
@Component({
selector: "wrapped-input",
template: `
<mat-form-field class="example-full-width">
<mat-label>Wrapped input</mat-label>
<!--We pass NgControl to regular MatInput -->
<input matInput [formControl]="ngControl.control" />
</mat-form-field>
`
})
export class WrappedInput {
constructor(@Self() @Optional() public ngControl: NgControl) {
if (this.ngControl) {
// Note: we provide the value accessor through here, instead of
// the `providers` to avoid running into a circular import.
// And we use NOOP_VALUE_ACCESSOR so WrappedInput don't do anything with NgControl
this.ngControl.valueAccessor = NOOP_VALUE_ACCESSOR;
}
}
}
That way Angular will think that WrappedInput
works like any other component that implements ControlValueAccessor
interface (MatInput
for example).
Plus, it will work with both ReactiveFormsModule
and regular FormsModule
.
WrappedInput
can be used like so:
<wrapped-input [formControl]="sourceControl"></wrapped-input>
Here's full working stackblitz that you can play with: https://stackblitz.com/edit/angular-wrapped-form-control-example?file=src/app/app.ts
Upvotes: 10
Reputation: 9784
You can also try implementing ControlValueAccessor Interface Class.
@Component({
selector: 'app-custom-input',
templateUrl: './app-custom-input.component.html'
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => HidInputFieldComponent)
}
]
})
export class HidInputFieldComponent implements OnInit, ControlValueAccessor {
@Output() setDropdownEvent: EventEmitter<any> = new EventEmitter();
ngOnInit() {}
writeValue(value: any) {}
propagateChange(time: any) {
console.log(time);
}
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched() {}
}
Upvotes: 0
Reputation: 27471
Use FormGroupDirective
This directive accepts an existing FormGroup instance. It will then use this FormGroup instance to match any child FormControl, FormGroup, and FormArray instances to child FormControlName, FormGroupName, and FormArrayName directives.
Doing this you can access child formControl from parent
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.html',
viewProviders:[{ provide: ControlContainer, useExisting: FormGroupDirective}]
})
export class HidInputFieldComponent {
constructor(private fcd:FormGroupDirective) {
}
ngOnInit() {
this.fcd.form.addControl('formControl',new FormControl(''));
}
}
<div class="container">
<input [formControl]="formControl"/>
<label>label</label>
</div>
Upvotes: 4