❓ 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 (with ControlValueAccessor)

  • custom-button (without ControlValueAccessor).

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>