🅰️ Angular Q113 / 128

How do you implement custom form controls?

AI-Powered Answer ✓ Answered

Custom form controls in Angular allow you to integrate your own UI components seamlessly with Angular's reactive or template-driven forms, making them behave like native input elements. This is achieved by making your component act as a `ControlValueAccessor`.

Core Concept: ControlValueAccessor

To make a custom component compatible with Angular forms, it must implement the ControlValueAccessor interface. This interface acts as a bridge, allowing Angular's FormControl (or ngModel) to read and write values to your custom component, and to register callbacks for changes and touch events.

Steps to Implement a Custom Control

1. Create Your Component

Start with a standard Angular component. This component will encapsulate your custom UI and logic.

typescript
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
export class CustomInputComponent implements ControlValueAccessor {
  // Component properties and methods will go here
}

2. Provide NG_VALUE_ACCESSOR

Register your component as a ControlValueAccessor using the NG_VALUE_ACCESSOR token in the component's providers array. useExisting refers to the component class itself, and multi: true is crucial because there can be multiple ControlValueAccessor implementations in an application (though only one per component).

typescript
providers: [
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
  }
]

3. Implement ControlValueAccessor Methods

The ControlValueAccessor interface requires four methods that facilitate communication between the form control and your component:

  • writeValue(obj: any): Called by Angular forms to write a value to the component. Use this to update your component's internal state or UI.
  • registerOnChange(fn: any): Registers a callback function (fn) that Angular will call when the control's value changes (i.e., when your custom component wants to notify the form).
  • registerOnTouched(fn: any): Registers a callback function (fn) that Angular will call when the control has been touched (i.e., when your custom component receives a blur event).
  • setDisabledState?(isDisabled: boolean): (Optional) Called by Angular to enable or disable the control. Use this to update your component's UI to reflect its disabled state.
typescript
export class CustomInputComponent implements ControlValueAccessor {
  @Input() label: string = ''; // Example input for the label
  value: any = ''; // Internal value representing the form control's value
  onChange: any = () => {}; // Callback to notify Angular of value changes
  onTouched: any = () => {}; // Callback to notify Angular of 'touched' state
  isDisabled: boolean = false; // Internal state for disabled

  // Called by the forms API to write to the view
  writeValue(obj: any): void {
    this.value = obj;
  }

  // Called by the forms API to register a callback function
  // that should be called when the control's value changes in the UI
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  // Called by the forms API to register a callback function
  // that should be called when the control receives a blur event.
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  // Called by the forms API to enable or disable the control
  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

  // Method to emit changes from your internal UI element (e.g., an input event)
  onInput(event: Event) {
    const newValue = (event.target as HTMLInputElement).value;
    this.value = newValue; // Update internal value
    this.onChange(newValue); // Notify Angular forms of the change
  }
}

4. Create the Template

Your component's template will contain the UI elements that represent your custom control. Bind to the value property and call onInput (or similar) when the internal value changes, and onTouched on blur.

html
<!-- custom-input.component.html -->
<div class="custom-input-container">
  <label *ngIf="label">{{ label }}</label>
  <input
    type="text"
    [value]="value"
    (input)="onInput($event)"
    (blur)="onTouched()"
    [disabled]="isDisabled"
    placeholder="Enter something..."
  />
</div>

Usage in Parent Form

Once implemented, your custom control can be used in Angular forms just like a native input, with formControlName (for reactive forms) or ngModel (for template-driven forms).

Parent Component HTML

html
<!-- app.component.html or parent-form.component.html -->
<form [formGroup]="myForm">
  <app-custom-input label="My Custom Field" formControlName="myCustomField"></app-custom-input>

  <div *ngIf="myForm.get('myCustomField')?.invalid && myForm.get('myCustomField')?.touched">
    <p style="color: red;">Field is required!</p>
  </div>

  <p>Form Value: {{ myForm.value | json }}</p>
</form>

Parent Component TypeScript

typescript
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-parent-form',
  templateUrl: './parent-form.component.html'
})
export class ParentFormComponent implements OnInit {
  myForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    this.myForm = this.fb.group({
      myCustomField: ['', Validators.required]
    });
  }
}