Building a Custom Enable/Disable + Input Field Component in Angular
Tags: angular, typescript, frontend
Published: 2024-07-06 22:35:40

Working with custom form components in Angular can be a game-changer for creating reusable and maintainable code. Custom components not only encapsulate complex form logic but also enhance code readability and modularity. Today, let's dive into creating a custom field component that includes a switch and a number input. This component will work as an Adapter, integrating seamlessly with Angular's reactive forms.

### The Scenario
We need a field that lets the user check a box to enable a number input, and if its enabled, get its value. Breaking it down, we need:

- A switch (checkbox) to enable or disable a number input.
- When the switch is off, the number input should be disabled, and its value should be set to undefined.
- The form should have the value 0 when the switch is off, and the number input's value when the switch is on.

As we can see, we have 2 fields (the checkbox and the number input) but we want only one value in the form; and that value isn't always 1 to 1 the value of the number input. To handle this logic, we will create a Custom Field Component to work as an Adapter.

Let's get started!

#### Step 1: Generate the Custom Component
First, create a new component called custom-field:

```
ng generate component custom-field
```

#### Step 2: Implement the Component Logic
We'll implement `ControlValueAccessor` interface to hook our custom component into Angular's reactive forms. By doing this, our component will act as an adapter, translating the complex internal state into a form-compatible interface.

The ControlValueAccessor interface allows Angular to communicate with custom form components. It acts as an adapter between Angular's form API and the custom component, providing methods to read from and write to the form control. It consists on 4 methods: `writeValue(obj: any)`, `registerOnChange(fn: any)`, `registerOnTouched(fn: any)`, `setDisabledState?(isDisabled: boolean)`, the last being optional.
- **writeValue(obj: any)**: This method is used to update the component's value when the form control's value changes.
- **registerOnChange(fn: any)**: This method is used to register a callback function that Angular will call when the component's value changes.
- **registerOnTouched(fn: any)**: This method is used to register a callback function that Angular will call when the component is touched.
- **setDisabledState?(isDisabled: boolean)**: This optional method is used to update the component's disabled state.

> For an easier understanding of the usage of the interface, we can compare `writeValue()` with a component `@Input` and `onChange()` with a component `@Output`.

custom-field.component.html:
``` html
<div>
  <label>
    <input
      type="checkbox"
      [(ngModel)]="isSwitchOn"
      (change)="onSwitchChange()"
    />
    Enable Number Input
  </label>
  <input
    type="number"
    [(ngModel)]="numberValue"
    [disabled]="!isSwitchOn"
    (ngModelChange)="onNumberChange($event)"
  />
</div>

```
custom-field.component.ts:
``` ts
@Component({
  selector: 'app-custom-switch-input-field',
  templateUrl: './custom-switch-input-field.component.html',
  styleUrls: ['./custom-switch-input-field.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomSwitchInputFieldComponent),
      multi: true,
    },
  ],
})
export class CustomSwitchInputFieldComponent implements ControlValueAccessor {
  isSwitchOn = false;
  numberValue: number | undefined;

  private onChange: any = () => {};
  private onTouched: any = () => {};

  writeValue(value: number): void {
    this.isSwitchOn = value !== 0;
    this.numberValue = this.isSwitchOn ? value : undefined;
  }

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

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

  onSwitchChange() {
    if (!this.isSwitchOn) {
      this.numberValue = undefined;
      this.onChange(0);
    } else {
      this.numberValue = 0;
    }
    this.onTouched();
  }

  onNumberChange(value: number) {
    if (this.isSwitchOn) {
      this.onChange(value);
    }
    this.onTouched();
  }
}

```

#### Step 3: Integrate with the Parent Component
Let's integrate with a parent component to use our custom field component within a reactive form.

parent.html:
``` html
<form [formGroup]="form">
  <app-custom-switch-input-field formControlName="customField"></app-custom-switch-input-field>
</form>
```
parent.ts:
``` ts
  form!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.form = this.fb.group({
      customField: [0], // Default value
    });
  }
```

### Examples and Use Cases

Here are a few more examples of how the custom field component can be used in different scenarios:

**Example 1: Configuring Product Options**
Imagine a form for configuring product options where certain features can be enabled or disabled, and the corresponding input fields are adjusted accordingly. This is useful in e-commerce platforms where products can have optional add-ons.

**Example 2: Conditional Form Fields**
In a survey form, certain questions may appear only if specific options are selected. For instance, if a user selects "Yes" to a question about owning a vehicle, additional fields about the vehicle may appear. The custom field component can handle enabling and disabling input fields based on user selections.

### Conclusion
By implementing ControlValueAccessor, we created a custom field component that integrates smoothly with Angular's reactive forms. Acting as an adapter, this component translates a complex internal state into a form-compatible interface. This approach encapsulates the logic of the field, enhances reusability and ensures that our forms are easier to maintain and extend. [You can check the full code on github.](https://github.com/mateuscechetto/custom-field-switch-input)