linkdin
Tomer Kamar Frontend Developer @ abra R&D
אוקטובר 28, 2024

 

Building Custom Form Controls in Angular

Have you ever struggled to create a unique custom form control in Angular that meets the specific needs of your application? If so, you're not alone! Custom form controls can transform your user interface by enhancing functionality and usability. This article will explore how to build custom form controls in Angular, complete with practical code examples. Whether you're a seasoned frontend developer or a backend developer venturing into the world of Angular, this guide will provide valuable insights and step-by-step instructions for creating your form controls.

 

Understanding Angular Form

Before diving into custom form controls, it's essential to grasp how Angular manages forms. Angular offers two main approaches to handling forms:

1. Reactive Forms
2. Template-driven Forms

Reactive Forms provide more control over form logic and state, making them ideal for complex forms. On the other hand, Template-driven Forms are simpler and more intuitive than straightforward forms.

 

Reactive Forms vs. Template-driven Forms

Reactive Forms are built using the FormControl and FormGroup classes, allowing developers to manage the form state and validation programmatically. On the other hand, template-driven forms leverage Angular directives in the template to create forms more declaratively. While both approaches are powerful, this guide will primarily focus on Reactive Forms as they offer a more robust structure for creating custom controls.

 

Creating a Custom Form Control

To create a custom form control, you need to implement the ControlValueAccessor interface. This interface connects the Angular form API with your custom component, allowing it to act like a standard form control.

Step 1: Set Up Your Angular Project

ng new custom-form-control-demo
cd custom-form-control-demo

Step 2: Generate the Custom Control Component 

Next, generate a new component for your custom form control. For this example, let’s create a simple custom checkbox component. 

ng generate component custom-checkbox

Step 3: Implement the ControlValueAccessor Interface 

In the newly created custom-checkbox.component.ts file, implement the ControlValueAccessor interface: 

writeValue: this method is called by the Forms module to write a value into a form control. 

registerOnChange: When a form value changes due to user input, we need to report the value back to the parent form. This is done by calling a callback, that was initially registered with the control using the registerOnChange method. 

registerOnTouched: When the user first interacts with the form control, the control is considered to have the status touched, which is useful for styling. To report to the parent form that the control was touched, we need to use a callback registered using the registerOnToched method. 

setDisabledState: form controls can be enabled and disabled using the Forms API. This state can be transmitted to the form control via the setDisabledState method. 

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

@Component({
  selector: 'app-custom-checkbox',
  template: `
    <label>
      <input type="checkbox" [checked]="value" (change)="onChange($event.target.checked)">
      {{ label }}
    </label>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomCheckboxComponent),
      multi: true,
    },
  ],
})
export class CustomCheckboxComponent implements ControlValueAccessor {
  value = false;
  label = 'Check me';

  onChange = (value: boolean) => {};
  onTouched = () => {};

  writeValue(value: boolean): void {
    this.value = value;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    // Handle the disabled state if needed
  }
}

Step 4: Use Your Custom Control in a Reactive Form 

Now that your custom checkbox is ready, you can integrate it into a reactive form. Start by updating the app.component.ts file: 

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

@Component({
  selector: 'app-root',
  template: `
    <form [formGroup]="form">
      <app-custom-checkbox formControlName="agree"></app-custom-checkbox>
      <button type="submit" (click)="submit()">Submit</button>
    </form>
  `,
})
export class AppComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      agree: [false],
    });
  }

  submit() {
    console.log(this.form.value);
  }
}

 

Benefits of Custom Form Controls

Creating custom form controls offers several advantages: 

Reusability: Once created, a custom form control can be reused across different components. 

Encapsulation: Custom controls allow for better encapsulation of functionality, making your application more modular. 

Custom Logic: You can easily implement custom validation and behavior tailored to your application's needs.

 

Validation 

Adding validation to your custom form control is essential for ensuring data integrity. You can implement Angular's built-in validators or create custom validation logic by extending the ControlValueAccessor methods. 

 

Accessibility 

When building custom controls, always consider accessibility. Make sure your custom components are keyboard navigable and provide appropriate ARIA attributes. 

 

Performance Optimization 

For complex forms with many custom controls, consider performance optimizations, such as chang detection strategies, to ensure your application remains responsive. 

 

Demo

Let’s create an example of a custom form control using the ControlValueAccessor interface. In this example, we will build a custom star rating component that allows users to select a rating from 1 to 5 stars. This component can be reused in any Angular form. 

Step 1: Generate the Custom Star Rating Component 

ng generate component star-rating

Step 2: Implement the ControlValueAccessor Interface 

Now, let's implement the ControlValueAccessor in the star-rating.component.ts file. This will allow the star rating component to integrate seamlessly with Angular forms.

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

@Component({
  selector: 'app-star-rating',
  template: `
    <div class="star-rating">
      <span 
        *ngFor="let star of stars; let index = index"
        (click)="rate(index + 1)"
        (mouseover)="hoverRating(index + 1)"
        (mouseleave)="clearHover()"
        [class.filled]="index < rating"
        [class.hovered]="index < hoverRatingValue">
        ★
      </span>
    </div>
  `,
  styles: [
    `
      .star-rating {
        font-size: 30px;
        cursor: pointer;
      }
      .filled {
        color: gold;
      }
      .hovered {
        color: lightgray;
      }
    `,
  ],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => StarRatingComponent),
      multi: true,
    },
  ],
})
export class StarRatingComponent implements ControlValueAccessor {
  stars: number[] = [1, 2, 3, 4, 5];  // Array to represent 5 stars
  rating: number = 0;  // Holds the current rating value
  hoverRatingValue: number = 0;  // Holds the value for hovered star

  // Callbacks to notify the form of changes
  onChange = (rating: number) => {};
  onTouched = () => {};

  // This method is called by the form to set the value
  writeValue(value: number): void {
    this.rating = value || 0;
  }

  // This method is called by the form to register a change handler
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  // This method is called by the form to register a touch handler
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  // Call this method when the component is disabled
  setDisabledState?(isDisabled: boolean): void {
    // Optionally handle the disabled state
  }

  // Method to set the rating when a star is clicked
  rate(value: number) {
    this.rating = value;
    this.onChange(this.rating);
    this.onTouched();
  }

  // Method to handle hover effect
  hoverRating(value: number) {
    this.hoverRatingValue = value;
  }

  // Method to clear the hover effect
  clearHover() {
    this.hoverRatingValue = 0;
  }
}

Explanation of the Code: 

ControlValueAccessor Implementation: The component implements the ControlValueAccessor interface, which includes methods for writeValue, registerOnChange, registerOnTouched, and an optional setDisabledState.

Star Rating Logic: 

– The stars array defines the number of stars (1 to 5). 

– The rate method sets the selected rating and notifies the Angular form about the change. 

– The hoverRating and clearHover methods provide visual feedback when the user hovers over the stars. 

Template and Styling: The component's template uses a loop to create star elements, which can be clicked to set the rating. The stars are styled to change color based on the rating. 

Step 3: Use the Star Rating Component in a Reactive Form 

Now, let’s use the custom star rating component in an Angular form. We’ll modify the app.component.ts file to include our star rating control. 

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

@Component({
  selector: 'app-root',
  template: `
    <h1>Star Rating Form</h1>
    <form [formGroup]="form" (ngSubmit)="submit()">
      <label for="rating">Select Rating:</label>
      <app-star-rating formControlName="rating"></app-star-rating>
      <div *ngIf="form.get('rating')?.invalid && form.get('rating')?.touched">
        Rating is required.
      </div>
      <button type="submit" [disabled]="form.invalid">Submit</button>
    </form>

    <h2>Submitted Value: {{ submittedRating }}</h2>
  `,
})
export class AppComponent {
  form: FormGroup;
  submittedRating: number | null = null;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      rating: [null, Validators.required],  // Rating is a required field
    });
  }

  submit() {
    this.submittedRating = this.form.value.rating;
    console.log('Submitted Rating:', this.submittedRating);
  }
}

Explanation of the Code: 

Reactive Form Setup: A reactive form is created with a single control named rating, which is linked to our star rating component. 

Validation: The rating control has a required validator, and an error message is displayed if the user tries to submit without selecting a rating. 

Form Submission: Upon submission, the selected rating is logged to the console and displayed in the template. 

 

Conclusion 

Angular opens up numerous opportunities for enhancing user experiences through the customization of form controls. In this guide, we have described a number of steps that can be followed in order to create components which are both reusable as well as modular and fit your specific requirements. As you go on learning what Angular can do, it is important to remember that practice is the most effective way of studying it.

For further assistance, feel free to contact us for expert help. You can also visit here to learn more about us!

Good Luck 🙂