I am attempting to create a reusable custom-input component that does not have the formControlName hard coded anywhere in the custom-input component (I have found some examples where the formControlName is hardcoded in the custom component). I want to be able to supply only the formControlName to the component, so that the same component could be used for multiple FormControls within one FormGroup. Here is a Stackblitz, and the relevant code is below:
/// CustomInputComponent ///
import { Component, forwardRef, Input, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
@Component({
standalone: true,
selector: 'custom-input',
template: ` <input
class="input"
type="text"
[disabled]="disabled"
(input)="updateValue($event)"
[value]="value"
/>`,
styles: [],
imports: [CommonModule, FormsModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomInputComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: CustomInputComponent,
},
],
})
export class CustomInputComponent implements ControlValueAccessor {
@ViewChild('input', { static: true }) input!: ElementRef<HTMLInputElement>;
@Input() ctrlConfigData!: any;
@Input() ctrlName!: string;
@Input() value = '';
disabled: boolean = false;
onChanged: any = () => {};
onTouched: any = () => {};
writeValue(value: string): void { // Update component state
console.log('writeValue() called with:', value);
this.value = value;
}
registerOnChange(fn: any): void { // Register the callback for value changes
this.onChanged = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
updateValue(event: Event): void {
const newValue = (event.target as HTMLInputElement).value;
console.log('newValue: ' + newValue);
this.value = newValue;
this.onChanged(newValue);
this.onTouched();
}
}
Here is the parent component, which creates the FormGroup and instantiates the custom component:
/// ContainerComponent ///
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormGroup, FormControl } from '@angular/forms';
import { CustomInputComponent } from '../custom-input/custom-input.component';
@Component({
standalone: true,
selector: 'container',
template: `
<custom-input
formControlName="ctrlA"
</custom-input>
`,
styles: [],
imports: [CommonModule, CustomInputComponent],
})
export class ContainerComponent implements OnInit{
fg: FormGroup = new FormGroup({});
ngOnInit() {
console.log('ContainerComponent instantiated');
this.fg.addControl('ctrlA',
new FormControl({ disabled: false, value: 'ctrlA!!' }));
this.fg.addControl('ctrlB',
new FormControl({ disabled: false, value: 'ctrlB' }));
}
}
Thanks.
You have to import ReactiveFormsModule
to the component, to use the formGroup
directive.
@Component({
standalone: true,
selector: 'container',
templateUrl: './container.component.html',
styleUrls: ['./container.component.scss'],
imports: [CommonModule, CustomInputComponent, ReactiveFormsModule],
})
Then make sure you wrap the custom inputs in a formGroup directive.
<form [formGroup]="fg">
<custom-input
formControlName="ctrlA"
[ctrlConfigData]="{ label: 'CTRL A' }"
[ctrlName]="'ctrlA'"
>
</custom-input>
<custom-input
formControlName="ctrlB"
[ctrlConfigData]="{ label: 'CTRL B' }"
[ctrlName]="'ctrlB'"
></custom-input>
</form>
Finally, I removed the validators providers property, since it's only needed when you provide custom validators.
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomInputComponent),
},
// {
// provide: NG_VALIDATORS,
// multi: true,
// useExisting: CustomInputComponent,
// },
],
})
import {
Component,
forwardRef,
Input,
ViewChild,
ElementRef,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormsModule,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
} from '@angular/forms';
@Component({
standalone: true,
selector: 'custom-input',
template: ` <input
class="input"
type="text"
(focus)="onTouched()"
[disabled]="disabled"
(input)="updateValue($event)"
[value]="value"
/>`,
// templateUrl: './custom-input.component.html',
styles: [],
imports: [CommonModule, FormsModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomInputComponent),
},
// {
// provide: NG_VALIDATORS,
// multi: true,
// useExisting: CustomInputComponent,
// },
],
})
export class CustomInputComponent implements ControlValueAccessor {
@ViewChild('input', { static: true }) input!: ElementRef<HTMLInputElement>;
@Input() ctrlConfigData!: any;
@Input() ctrlName!: string;
@Input() value = '';
disabled: boolean = false;
onChanged: any = () => {};
onTouched: any = () => {};
writeValue(value: string): void {
// Update component state
console.log('writeValue() called with:', value);
this.value = value;
}
registerOnChange(fn: any): void {
// Register the callback for value changes
this.onChanged = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
updateValue(event: Event): void {
const newValue = (event.target as HTMLInputElement).value;
console.log('newValue: ' + newValue);
this.value = newValue;
this.onChanged(newValue);
this.onTouched();
}
}
<form [formGroup]="fg">
<custom-input
formControlName="ctrlA"
[ctrlConfigData]="{ label: 'CTRL A' }"
[ctrlName]="'ctrlA'"
>
</custom-input>
<custom-input
formControlName="ctrlB"
[ctrlConfigData]="{ label: 'CTRL B' }"
[ctrlName]="'ctrlB'"
></custom-input>
</form>
<!-- reset button -->
<div style="margin: 4rem 0 0 0"><button>Reset Form</button></div>
<!-- monitoring values -->
<div style="padding-top:1rem;">
<table border="1">
<tr>
<th style="padding: 10px">FormControl (ctrlA)</th>
<th style="padding: 10px">FormControl (ctrlB)</th>
</tr>
<tr>
<td style="padding: 10px">
touched : {{ fg.get('ctrlA')?.touched }} <br />
dirty : {{ fg.get('ctrlA')?.dirty }} <br />
valid : {{ fg.get('ctrlA')?.valid }} <br />
Value : {{ fg.get('ctrlA')?.value }} <br />
Required : {{ fg.get('ctrlA')?.hasError('required') }}
</td>
<td style="padding: 10px">
touched : {{ fg.get('ctrlB')?.touched }} <br />
dirty : {{ fg.get('ctrlB')?.dirty }} <br />
valid : {{ fg.get('ctrlB')?.valid }} <br />
Value : {{ fg.get('ctrlB')?.value }} <br />
Required : {{ fg.get('ctrlB')?.hasError('required') }}
</td>
</tr>
</table>
</div>
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
import { CustomInputComponent } from '../custom-input/custom-input.component';
@Component({
standalone: true,
selector: 'container',
templateUrl: './container.component.html',
styleUrls: ['./container.component.scss'],
imports: [CommonModule, CustomInputComponent, ReactiveFormsModule],
})
export class ContainerComponent implements OnInit {
fg: FormGroup = new FormGroup({});
ngOnInit() {
console.log('ContainerComponent instantiated');
this.fg.addControl(
'ctrlA',
new FormControl({ disabled: false, value: 'ctrlA!!' })
);
this.fg.addControl(
'ctrlB',
new FormControl({ disabled: false, value: 'ctrlB' })
);
}
resestForm() {
this.fg?.get('ctrlA')?.setValue('ctrlA reset!');
this.fg?.get('ctrlB')?.setValue('ctrlB reset!');
}
}