Building your own form fields the Angular way with control value accessors
Tags: angular, tutorial, webdev, javascript
Published: 2022-10-31 05:50:38

Angular forms are powerful and, well known, can be used in a way that greatly simplify your own forms by also improving the reusability of your components.

We will see how by implementing our own form with our own custom field and see how we can retrieve its value.

---

## Scenario

We will be creating a reclamation form.

From our page, a customer will be able to specify its invoice id which has some validation and logic such as:

- **A prefix** since all of our invoice ids start with `INV-`
- **A length** our invoice id is at most 8 chars long

## Creating our initial form

Let's write our first version only with reactive forms.

We will need a `FormGroup` that is wrapping our `invoiceId` field:

```ts
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  form: FormGroup<{ invoiceId: FormControl<string | null> }>;

  constructor(fb: FormBuilder) {
    this.form = fb.group({
      invoiceId: ['', [Validators.pattern(/^INV-.*/), Validators.maxLength(8)]],
    });
  }
}
```

and our associated HTML:

```html
<h1>Invoice reclamation</h1>

<form [formGroup]="form">
  <label for="invoice-id">Invoice ID: </label>
  <input
    type="text"
    formControlName="invoiceId"
    id="invoice-id"
    placeholder="INV-xxxx"
  />

  <button type="submit" [disabled]="!form.valid">Send</button>
</form>
```

> If you want to see what's going on internally, you may want to add this bit of code at the end of your template:

> ```html
<hr />
<p>Value:</p>
<pre>{{ form.value | json }}</pre>
<p>Errors:</p>
<pre>{{ form.controls.invoiceId.errors | json }}</pre>
```

So far so good:

![Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gfl1ov65rwd0pbyc2m8y.gif)

## Enhancing the field

We might want to help the user filling this form

For example, if we could specify ourselves the prefix it could be great. Even better, we could show him the subject of his invoice when he types it to offer him some context.

Let's modify our code a bit to achieve this:

```ts
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  form: FormGroup<{ invoiceId: FormControl<string | null> }>;

  constructor(fb: FormBuilder) {
    this.form = fb.group({
      invoiceId: ['', Validators.maxLength(4)],
    });
  }
}
```

```html
<h1>Invoice reclamation</h1>

<form [formGroup]="form">
  <label for="invoice-id">Invoice ID: </label>
  <span style="font-family: 'Courier New', Courier, monospace">INV-</span>
  <input
    type="text"
    formControlName="invoiceId"
    id="invoice-id"
    placeholder="xxxx"
  />

  <p *ngIf="form.controls.invoiceId.value && form.controls.invoiceId.valid">INVOICE n°...</p>
  
  <button type="submit" [disabled]="!form.valid">Send</button>
</form>
```

That's a bit better for the user:

![with enhanced behavior](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18fzzmcsabdsn9fpdkha.gif)

However for us, not so much

By doing this we:

- Introduced more complexity in our form even tho this behavior is only related to a specific field
- We changed the expected value of our `invoiceId` field: if somebody that is dealing with it elsewhere is used to see it starts with `INV-`, he probably won't get at first glance why we aren't enforcing it in our validators

Fortunately, Angular offers a way for developers to write their own form fields and encapsulate them, let's do that!

## The Control Value Accessor

### Introduction

The [`ControlValueAccessor` interface](https://angular.io/api/forms/ControlValueAccessor) is a very handy interface that helps us writing our form fields

It exposes four methods (including with an optional one):

- **`writeValue(obj: any): void`**
  That is called whenever a new value is provided to our field from the forms API
- **`registerOnChange(fn: any): void`**
  Provides the function that we will have to call when we will be making any change to our field
- **`registerOnTouched(fn: any): void`**
  Same as the previous one but for the `touched` behavior
- **`setDisabledState(isDisabled: boolean)?: void`**
  Called whenever the forms API informs our field that the disabled state has changed so that we can apply it to our field

### Applied to our field

#### Creating a new component

In a new `InvoiceIdFormField` component that will be holding our field, let's move our logic there.

We will just replace the whole form by a simple `FormControl` since we will only be manipulating the invoice id here:

```ts
@Component({
  selector: 'app-invoice-id-form-field',
  templateUrl: './invoice-id-form-field.component.html',
})
export class InvoiceIdFormFieldComponent {
  readonly invoiceIdControl: FormControl<string | null>;

  constructor() {
    this.invoiceIdControl = new FormControl<string | null>(
      null,
      Validators.maxLength(4)
    );
  }
}
```

```html
<label for="invoice-id">Invoice ID: </label>
<span style="font-family: 'Courier New', Courier, monospace">INV-</span>
<input
  type="text"
  [formControl]="invoiceIdControl"
  id="invoice-id"
  placeholder="xxxx"
/>

<p *ngIf="invoiceIdControl.value && invoiceIdControl.valid">INVOICE n°...</p>
```

#### Implementing `ControlValueAccessor`

Now that our component is created, we can implement the interface:

```diff
@Component({
  selector: 'app-invoice-id-form-field',
  templateUrl: './invoice-id-form-field.component.html',
})
export class InvoiceIdFormFieldComponent implements ControlValueAccessor {
+ private _onChange: (invoiceId: string | null) => any = noop;
+ private _onTouched: () => any = noop;

  readonly invoiceIdControl: FormControl<string | null>;

  constructor() {
    this.invoiceIdControl = new FormControl<string | null>(
      null,
      Validators.maxLength(4)
    );
  }

+ writeValue(invoiceId: string | null): void {
+   this.invoiceIdControl.setValue(invoiceId);
+ }

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

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

+ setDisabledState?(isDisabled: boolean): void {
+   isDisabled
+     ? this.invoiceIdControl.disable()
+     : this.invoiceIdControl.enable();
+ }
}
```

Since everything is ready for Angular to accept our component as a field, we now have to propagate the value when it is set:

```ts
export class InvoiceIdFormFieldComponent implements OnDestroy, ControlValueAccessor {  
  // ...
  private readonly _componentDestroyed$ = new Subject();

  constructor() {
    this.invoiceIdControl = new FormControl<string | null>(
      null,
      Validators.maxLength(4)
    );

    this.invoiceIdControl.valueChanges.pipe(
      map(invoiceId => (invoiceId ? 'INV-' : '') + invoiceId),
      takeUntil(this._componentDestroyed$),
    ).subscribe(invoiceId => {
      this._onChange(invoiceId);
      this._onTouched();
    });
  }

  ngOnDestroy(): void {
    this._componentDestroyed$.next('');
    this._componentDestroyed$.complete();
  }

  // ...
```

> Since we are manipulating our value internally, we can now prepend the `INV-` prefix ourselves

Last but not least, we now have to indicate to Angular's form API how to use our component as a field:

```diff
@Component({
  selector: 'app-invoice-id-form-field',
  templateUrl: './invoice-id-form-field.component.html',
+ providers: [
+   {
+     provide: NG_VALUE_ACCESSOR,
+     useExisting: forwardRef(() => InvoiceIdFormFieldComponent),
+     multi: true
+   }
+ ]
})
```

#### Using our form field

Great! Now that this field and its logic is encapsulated, we can use it from our main form.

Let's start by simplifying the validators:

```ts
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {
  form: FormGroup<{ invoiceId: FormControl<string | null> }>;

  constructor(fb: FormBuilder) {
    this.form = fb.group({ invoiceId: '' });
  }
}
```

And then we can just replace our previous template simply by our field:

```html
<h1>Invoice reclamation</h1>

<form [formGroup]="form">
  <app-invoice-id-form-field
    formControlName="invoiceId"
  ></app-invoice-id-form-field>

  <button type="submit" [disabled]="!form.valid">Send</button>
</form>
```

Let's test it:

![With CVA](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5vn2mzbpyz4b09dqtdjo.gif)

We can see that our component is recognized by Angular as a form field and can be bound to our form value.

> Even if we are using a `FormGroup` there, you can also use `[(ngModel)]` if you only have a single value, no need to change anything

### Propagating the errors

This is great, however, you might have noticed that breaking a validator of our `invoiceIdControl` does not propagate the error to the englobing form.

This is surprisingly harder that what we achieved so far and we will need to change the way our control value accessor is defined to do so.

For this to work, we will need to retrieve the `NgControl` Angular is using for this component by capturing it using the dependency injection and reroute its accessor to the component itself:

```diff
  constructor(
+   @Self()
+   private readonly control: NgControl
  ) {
+   this.control.valueAccessor = this;

    this.invoiceIdControl = new FormControl<string | null>(
      null,
-     Validators.maxLength(4)
+     Validators.maxLength(8)
    );

    this.invoiceIdControl.valueChanges
      .pipe(
        map((invoiceId) => (invoiceId ? 'INV-' : '') + invoiceId),
        takeUntil(this._componentDestroyed$)
      )
      .subscribe((invoiceId) => {
        this._onChange(invoiceId);
        this._onTouched();
      });
  }
```

However, by doing so, the component will be injecting himself in its constructor, resulting in a circular dependency. In order to break it, we will have to remove it from the `NG_VALUE_ACCESSOR` provider:

```diff
@Component({
  selector: 'app-invoice-id-form-field',
  templateUrl: './invoice-id-form-field.component.html',
- providers: [
-   {
-     provide: NG_VALUE_ACCESSOR,
-     useExisting: forwardRef(() => InvoiceIdFormFieldComponent),
-     multi: true,
-   },
- ],
})
```

Finally, we will need to assign the validators of the captured control to the ones used by the component internally:

```ts
  // ...

  ngOnInit(): void { this.control.control?.setValidators([this.invoiceIdControl.validator!]);
    this.control.control?.updateValueAndValidity();
  }

  // ...
```

And *voilà*, Angular is now able to interact with the control to retrieve its value and its errors:

![Errors propagated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/myxigd5tf251cltj5m6c.gif)

## Take aways

In this post we saw:

- How to extract the logic and the template bound to a specific field out of a form
- How to make a component act like a form field for the Angular form API
- How to propagate errors from our custom form field to the main form

## Final words

Extracting custom form fields can really help you to keep your codebase clean and straightforward. By doing so, you will end up with clearer forms that does not deal with the extra complexity of every fields, and components that are clearly dealing *only* with what they are supposed to.

Our example is pretty simple but this can also result in a better user experience if you are designing your field adequately. For example, for this invoice, we could have added a link to an external service when identifying an invoice, a PDF preview of it or extra validation to help the user using this field: using a dedicated component helps you to unleash the full power of Angular inside it.

However, keep in mind that this is still a form field and you should be able to establish boundaries: making everything a form field might be as painful as not having any since you will deal with a lot of new layers.

---

> Photo by <a href="https://unsplash.com/@romaindancre?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Romain Dancre</a> on <a href="https://unsplash.com/s/photos/paperwork?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>