❓ Question
Q: We can create custom components without ControlValueAccessor
, so why use it?
A: You use ControlValueAccessor
when you want your custom component to work seamlessly with Angular forms (ngModel
, reactive forms), so it can be used just like built-in form controls (<input>
, <select>
, etc.) and participate in form validation and value tracking.
Without ControlValueAccessor
, your component can’t be used with Angular forms in this way.
❌ Without ControlValueAccessor
You cannot use [(ngModel)]
or formControl
with your custom component:
<!-- This will NOT work without ControlValueAccessor -->
<app-custom-input [(ngModel)]="inputValue"></app-custom-input>
✅ With ControlValueAccessor
You can use [(ngModel)]
just like a native input:
<!-- This works because ControlValueAccessor is implemented -->
<app-custom-input [(ngModel)]="inputValue"></app-custom-input>
🛠️ How to Implement ControlValueAccessor (Step-by-step)
We will create two components:
-
custom-input
(withControlValueAccessor
) -
custom-button
(withoutControlValueAccessor
).
You do not need ControlValueAccessor
for a button, as it does not manage or bind a value.
❓ Why not use ControlValueAccessor in button?
A: ControlValueAccessor
is for components that act as form controls—i.e., they hold and manage a value (like <input>
, <select>
, etc.).
A button:
-
Does not represent a value.
-
Is used to trigger actions (e.g., submit).
-
Communicates via events like
(click)
.
🔑 Summary:
-
✅ Use
ControlValueAccessor
for value-based controls (inputs, checkboxes). -
❌ Use
@Output
events for action-based components (buttons).
📁 1. Create Components
Create two components inside the app/
folder:
-
custom-input
-
custom-button
📄 2. custom-input.component.ts
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-custom-input',
standalone: false,
// templateUrl: './custom-input.html',
// styleUrl: './custom-input.css'
template: `
<label for="customInput">Custom Input:</label>
<input id="customInput" type="text" [value]="value" (input)="onInput($event)" />
`,
providers: [
{
provide: NG_VALUE_ACCESSOR, // Tells Angular this component is a custom form control
useExisting: forwardRef(() => CustomInput), // Use this class for the token
multi: true // Allow multiple value accessors (required) [this lets Angular keep a list of value accessors, not just one, It’s required for custom form controls using NG_VALUE_ACCESSOR]
}
]
})
export class CustomInput implements ControlValueAccessor {
value = ''; // Holds the current value of the input
// These are callbacks set by Angular forms to notify value/touch changes
OnChange = (value: string) => { }; // Called when value changes (TypeScript class property assigned to an arrow function (also called a lambda expression)
OnTouched = () => { }; // Called when control is touched
onInput(event: Event) {
const input = event.target as HTMLInputElement; // Get the input element from the event
this.value = input.value; // Update local value
this.OnChange(this.value); // Notify Angular forms about the value change
this.OnTouched(); // Notify Angular forms that the control was touched
}
// ControlValueAccessor methods:
writeValue(value: any): void {
// Called by Angular to update the input when form model changes
this.value = value || '';
}
registerOnChange(fn: any): void {
// Angular provides a function to call when value changes
this.OnChange = fn;
}
registerOnTouched(fn: any): void {
// Angular provides a function to call when control is touched
this.OnTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
// Optional: Called by Angular to enable/disable the input
}
}
📄 3. custom-button.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-custom-button',
standalone: false,
// templateUrl: './custom-button.html',
// styleUrl: './custom-button.css'
template: `<button type="button" (click)="onClick()">{{label}}</button>`,
})
export class CustomButton {
@Input() label = 'Submit'; // Allows parent to set the button text, default is 'Submit'
@Output() clicked = new EventEmitter<void>(); // Emits an event when the button is clicked
onClick() {
this.clicked.emit(); // Triggers the 'clicked' event for the parent to handle
}
}
🧩 4. Add FormsModule in app.module.ts
@NgModule({
declarations: [
App,
CustomInput,
CustomButton
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule // Add this line
],
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection()
],
bootstrap: [App]
})
export class AppModule {}
🧠 5. Logic in app.ts
export class App {
protected title = 'ngLab';
inputValue = '';
onSubmit() {
alert(`Submitted value: ${this.inputValue}`);
}
}
🖼️ 6. View Components in app.html
<p>Hello Shoyeb</p>
<app-custom-input [(ngModel)]="inputValue"></app-custom-input>
<!-- Here (clicked) is used to emit a custom event from the button component (app-custom-button) so the parent can react when the button is pressed. -->
<app-custom-button label="Submit Button" (clicked)="onSubmit()"></app-custom-button>
<p>You entered: {{ inputValue }}</p>