Building a Reusable MultiCheck Field in Angular
Tags: angular, forms, javascript, webdev
Published: 2020-09-10 06:01:08

*Cover photo by [Alev Takil](https://unsplash.com/@alevtakil?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/choices?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)*

It's quite common to have forms where the user can select many options from several available:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/e1c532jygdbccmt67nnr.gif)

The most popular way to tackle this in Angular is by using a set of `<input type="checkbox">` with a `FormArray`. However, when the same functionality is required in several forms across the application, it's highly possible we start repeating lots of code, for both the logic and the markup.

In this post we will address this issue by building a component that has the following features:

- MultiCheck: several options can be selected simultaneously

- Reusable: the options can be presented with different visual implementations without re-writing the logic

- Custom Field: tailored form field that works directly with Angular Forms

Once we are done, we could use the component to build forms that behave like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kepls3nh8lxape789j81.gif)

## Table of Contents

- Design

- Step 1: Supporting a **SimpleCheckOption** Component

- Step 2: Supporting Any Kind of Option Component

- Step 3: Integration with Angular Forms

- Final Words

- Demo

- Further Improvement

- Code Repository Links

## Design

Our component will be composed by two elements:

1. The **field** component, which keeps track of the selected options and provides the integration with AngularForms.

2. The **option** component,  which represents a single check option and provides the visual implementation for it. The idea is that we have several of this kind.

## Step 1: Supporting a SimpleCheckOption Component

We will start by supporting only a `simple-check-option` by our `multi-check-field`, but keeping in mind that we want the field to be used with any **option** component.

That being said, we'll use **Content Projection** to provide the desired options to the `multi-check-field`, like this:

```html
<multi-check-field>
    <simple-check-option *ngFor="let option of options" [value]="option"
      [label]="option.label">
    </single-check-option>
</multi-check-field>
```

Note how **Content Projection** is used by passing the options inside the enclosing tags of the `multi-check-field`. 

Now, let's see the implementation of the `simple-check-option`:

```javascript
@Component({
  selector: 'simple-check-option',
  template: `
    <label>
      <input type="checkbox" [formControl]="control">
      {{ label }}
    </label>
  `
})
export class SimpleCheckOptionComponent {

  @Input() value: any;
  @Input() label: string;

  public control = new FormControl(false);

  get valueChanges$(): Observable<boolean> {
    return this.control.valueChanges;
  }

}
```

The component has a standard `<input type="checkbox">` with it's label. We also declare a `FormControl` to manipulate the checkbox value and, additionally, we provide a `valueChanges$` accessor so we can interact with the component with type safety from the outside.

The `multi-check-field` component will use the `ContentChildren` decorator to query the projected options:

```javascript
@Component({
  selector: 'multi-check-field',
  template: `<ng-content></ng-content>`
})
export class MultiCheckFieldComponent implements AfterContentInit {

  @ContentChildren(SimpleCheckOptionComponent)
  options!: QueryList<SimpleCheckOptionComponent>;

  ngAfterContentInit(): void {
    // Content query ready
  }

}
```

It's worth to be noted that the content query will first be ready to be used in the  `AfterContentInit` lifecycle, but not before. Additionally, see how we use the `<ng-content>` tags in the component's template to render there the provided content (the options).

Now, let's see how we keep track of the selected options

```javascript
private subscriptions = new Subscription();
private selectedValues: any[] = [];

ngAfterContentInit(): void {
  this.options.forEach(option => {
    this.subscriptions.add(
      option.valueChanges$.subscribe(
        (optionChecked) => {
          if (optionChecked) {
            this.add(option.value);
          } else {
            this.remove(option.value);
          }
        }
      )
    );
  });
}

private add(value: any): void {
  this.selectedValues.push(value);
}

private remove(value: any): void {
  const idx = this.selectedValues.findIndex(v => v === value);
  if (idx >= 0) {
    this.selectedValues.splice(idx, 1);
  }
}
```

We use the option's `valueChanges$` accessor to subscribe to the event when an option is checked/unchecked.  Depending on the `optionChecked` boolean value, we then proceed to add or remove this option from our `selectedValues` array.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wx4xigkulrnf3m8suofc.gif)

At this point, our `multi-check-field` is fully integrated with the `simple-check-option`. But we should take advantage of Angular's Content Projection to be able to support any kind of component as a *check-option*. Let's see how.

## Step 2: Supporting Any Kind of Option Component

Let's create a new **option** component that looks very different to the `simple-check-option` but has the same functionality. We'll name it `user-check-option` and it will represent... well, an user :sweat_smile:.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/s5ll70w6ve8pkpk1jh7n.gif)

The component logic is basically the same that we have in `simple-check-option`, but the template has considerable differences:

```javascript
@Component({
  selector: 'user-check-option',
  template: `
    <label>
      <input type="checkbox" [formControl]="control">
      <div class="card">
        <div class="avatar">
          <img src="assets/images/{{ value.avatar }}">
          <div class="span"></div>
        </div>
        <h1>{{ value.name }}</h1>
        <h2>{{ value.location }}</h2>
      </div>
    </label>
  `
})
export class UserCheckOptionComponent {

  @Input() value: any;

  public control = new FormControl(false);

  get valueChanges$(): Observable<boolean> {
    return this.control.valueChanges;
  }

}
```

To support our new `user-check-option` by the **field** component, we have to modify the `ContentChildren` query, given that we are not targeting exclusively a `SimpleCheckOption` anymore. This is the query we currently have:

```javascript
@ContentChildren(SimpleCheckOptionComponent)
options!: QueryList<SimpleCheckOptionComponent>;
```

Unfortunately, we cannot use `ContentChildren` to target two different kind of components, but we can use the power of [Angular's Dependency Injection (DI)](https://angular.io/guide/dependency-injection) to overcome this situation.

### Dependency Injection to the Rescue :man_firefighter: :woman_firefighter: :fire_engine:

One possible solution for this issue would be to use [alias providers](https://angular.io/guide/dependency-injection-in-action#alias-providers-useexisting) to create a common [DI token](https://angular.io/guide/dependency-injection#dependency-injection-tokens) to be employed by our **option** components.

```javascript
abstract class MultiCheckOption { }                        // (1)

@Component({
  selector: 'simple-check-option',
  providers: [
    {                                                      // (2)
      provide: MultiCheckOption,
      useExisting: SimpleCheckOptionComponent,
    }
  ]
})
export class SimpleCheckOptionComponent { ... }

@Component({
  selector: 'user-check-option',
  providers: [
    {                                                      // (3)
      provide: MultiCheckOption,
      useExisting: UserCheckOptionComponent
    }
  ]
})
export class UserCheckOptionComponent { ... }
```

1. We start by creating a `MultiCheckOption` class to be used as DI token by our **option** components.

2. We configure the injector at the component level of our `SimpleCheckOptionComponent` by using the [providers](https://angular.io/guide/dependency-injection-providers) metadata key. With this configuration, when Angular's DI ask our component's injector for an instance of `MultiCheckOption`, it would pass the existing instance of the component itself.

3. We do the same for the `UserCheckOptionComponent`.

The `ContentChildren` query could now be rewritten as:

```javascript
@ContentChildren(MultiCheckOption)
options!: QueryList<MultiCheckOption>;
```

But we are not finished yet... at this point we lost access to the members and methods of the **option** components, since the `MultiCheckOption` class is empty. We can fix this by using the class itself to hold what is common among the **options** and expose what necessary. After that, we take advantage of ES6 class inheritance to extend the `option` components from `MultiCheckOption`.

```javascript
export abstract class MultiCheckOption {
  abstract value: any;
  public control = new FormControl(false);
  get valueChanges$(): Observable<boolean> {
    return this.control.valueChanges;
  }
}

@Component(...)
export class SimpleCheckOptionComponent extends MultiCheckOption {
  @Input() value: any;
  @Input() label: string;
}

@Component(...)
export class UserCheckOptionComponent extends MultiCheckOption {
  @Input() value: any;
}
```

And just like that, the `multi-check-field` supports now any component that implements the `MultiCheckOption` logic.

## Step 3: Integration with Angular Forms

At this stage, you might try to use the `multi-check-field` with Angular Forms

```html
<multi-check-field formControlName="subjects">
    ...
</multi-check-field>
```

But then, you will get the following error:

> No value accessor for form control with name: 'subjects'

The reason is, the `AngularFormsModule` only knows how to deal with native form elements (like `<input>` and `<select>`). In order for our custom `multi-check-field` to work with Angular Forms, we'll have to tell the framework how to communicate with it. (If this is the first time you hear about custom form fields in Angular, I would recommend you to check this [post](https://netbasal.com/angular-custom-form-controls-made-easy-4f963341c8e2).

### 1. The `NG_VALUE_ACCESSOR` Provider

We start by registering the component with the global `NG_VALUE_ACCESSOR` provider:

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

@Component({
  selector: 'multi-check-field',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MultiCheckFieldComponent),
      multi: true
    }
  ]
})
export class MultiCheckFieldComponent { ... }
```

### 2 . The `ControlValueAccesor` Interface

Additionally, we need to implement the `ControlValueAccesor` interface, which defines the following set of methods to keep the **view** (our component) and the **model** (the form control) in sync.

```javascript
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
```

#### writeValue(obj: any)

This function is executed by the framework to set the field value from the **model** to the **view**. For example, when performing any of the following actions.

```javascript
multiCheckControl = new FormControl(TEST_INITIAL_VALUE);
multiCheckControl.setValue(TEST_VALUE);
multiCheckControl.patchValue(TEST_VALUE);
```

In our case, the `obj` parameter should be an array containing the selected options values. We better name it `values` for improved readability. 

```javascript
writeValue(values: any[]): void {
    this.selectedValues = [];
    values = values || [];
    values.forEach(selectedValue => {
      const selectedOption = this.options.find(v => v.value === selectedValue);
      selectedOption.control.setValue(true);
    });
}
```

Each item of the `values` array is mapped to the corresponding `option`, and then the checked value is reflected in it's view (in our example, this is done yet through another control).

Note that every time we call `selectedOption.control.setValue()`, the corresponding `valueChanges$` subscription declared in `ngAfterContentInit` is called and the option's value gets added to the local `selectedValues` array.

Let's see it working

```javascript
@Component({
  selector: 'app-root',
  template: `
    <multi-check-field [formControl]="multiCheckControl">
      <simple-check-option *ngFor="let subject of subjects"
        [value]="subject" [label]="subject.label">
      </simple-check-option>
    </multi-check-field>
    <button (click)="setTestValue()">Set Test Value</button>
    Control value: <pre>{{ multiCheckControl.value | json }}</pre>
  `,
})
export class AppComponent {

  public subjects = [
    { code: '001', label: 'Math' },
    { code: '002', label: 'Science' },
    { code: '003', label: 'History' },
  ];

  public multiCheckControl = new FormControl();

  setTestValue() {
    const testValue = [this.subjects[0], this.subjects[1]];
    this.multiCheckControl.setValue(testValue);
  }

}
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/edsgbr13iqvxqu6c04pb.gif)

#### registerOnChange(fn: any)

Registers the function that needs to be called when the field value changes in the UI. When the provided function is called, it will update the value from the **view** to the **model**.

In our case, we have to update the model value every time an **option** is checked/unchecked.

```javascript
export class MultiCheckFieldComponent implements ControlValueAccessor {

  _onChange: (_: any) => void;

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

  private add(value: any): void {
    this.selectedValues.push(value);
    this._onChange(this.selectedValues);
  }

  private remove(value: any): void {
    const idx = this.selectedValues.findIndex(v => v === value);
    if (idx >= 0) {
      this.selectedValues.splice(idx, 1);
      this._onChange(this.selectedValues);
    }
  }
  ...
}
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w9tnocc9em1lqub8rlv2.gif)

#### registerOnTouched(fn: any)

In the same way as the previous method, we need to register the function to be called when the field is touched, in order for the control to trigger validation and more.

We will leave the implementation of this method out of the scope of this tutorial.

#### setDisabledState?(isDisabled: boolean)

Last but no least, the `setDisabledState` method. This function is called when the field is enable/disabled programmatically. For example, when the following actions are performed:

```javascript
multiCheckControl = new FormControl({
  value: TEST_INITIAL_VALUE,
  disabled: true
});
multiCheckControl.disabled();
multiCheckControl.enabled();
```

This method will also be left out of the scope of the tutorial.

## Final Words

We managed to create a component that provides a multi-check functionality but also offers:

- Reducing of code duplication, given that all the logic is encapsulated within the component and doesn't need to be re-written for every form.

- Simplicity, since the usage is pretty straightforward. Very similar to a native `<select>` with `<option>` tags inside.

- Reusability, because the options can be styled as desired. 

- Compatibility, considering that it supports integration with Angular Forms.

## Demo Time :volcano:

{% stackblitz multi-check view=preview %}

## Further Improvement

There is still a lot of room for improvement. I list here some ideas in case you want to code a bit. Don't hesitate to open a PR to integrate your solution to the repository:

- Support a value passed on initialization (writeValue executed before ngAfterContentInit) :white_check_mark:

- Support changes in the projected options (when they are added or removed from DOM)

- Support registerOnTouched and setDisableState methods

- Write a minValuesLength and maxValuesLength validators

- Support passing a template as an option instead of a component

## Code Repository Links

- The full source code can be found [here](https://github.com/jcamilom/multi-check)

- In [this branch](https://github.com/jcamilom/multi-check/tree/further-improvement), you can find the implementation for some of the improvements suggested above