Angular Material Multi-Select Autocomplete
Tags: angular, material, select, javascript
Published: 2021-08-05 15:41:41

---
title: Angular Material Multi-Select Autocomplete
published: true
description: For a recent client, I needed a "searchable" select. They wanted to match functionality used in other applications. The original searchable selects were a legacy jQuery object that would have been an odd fit in a modern Angular application.
tags: Angular, Material, Select, JavaScript
series: Angular
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c71tg4g2qg0ie2xzy5er.png
---

For a recent client, I needed a "searchable" select. They wanted to match functionality used in other applications. The original searchable selects were a legacy jQuery object that would have been an odd fit in a modern Angular application.

What I needed was a ***select-type*** dropdown that allowed for multi-row selection, as well as the ability to filter the list down on a string entered as a search by the user.

Here is what I came up with ... a multiselect autocomplete.

## Code

[Working Example](https://bob-fornal.github.io/multi-select/)
[GitHub Repo](https://github.com/bob-fornal/multi-select/)

## HTML

Starting with the HTML ... these are displayed out of order to make the logic behind them more understandable.

### Input

This is the form field with a [Material](https://material.angular.io/) Input tied to `selectControl`.

```html
<mat-form-field class="full-width">
  <input matInput type="text"
  [placeholder]="placeholder"
  [matAutocomplete]="auto"
  [formControl]="selectControl">
</mat-form-field>
```

### Chip List

I added a [Material](https://material.angular.io/) Chip List to display the selections. This code is generally above the other code so that they are not hidden under the Autocomplete dropdown. This list also allows for Chips to be removed on click.

```html
<div class="chip-list-wrapper">
  <mat-chip-list #chipList>
    <ng-container *ngFor="let select of selectData">
      <mat-chip class="cardinal-colors" (click)="removeChip(select)">
        {{ select.item }}
        <mat-icon class="mat-chip-remove">cancel</mat-icon>
      </mat-chip>
    </ng-container>
  </mat-chip-list>
</div>
```

### Autocomplete

And, here is the [Material](https://material.angular.io/) Autocomplete tied to `filterdata`.

```html
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
  <mat-option *ngFor="let data of filteredData | async">
    <div (click)="optionClicked($event, data)">
      <mat-checkbox [checked]="data.selected" 
        (change)="toggleSelection(data)" 
        (click)="$event.stopPropagation()">
        {{ data.item }}
      </mat-checkbox>
    </div>
  </mat-option>
</mat-autocomplete>
```

## CSS

The CSS is pretty straight forward ... some sizing and color.

```css
.full-width {
  width: 100%;
}
​
.chip-list-wrapper {
  min-height: 3em;
}
​
.msac-colors {
  background-color: var(--primary-color);
  color: white;
}
```

## TypeScript

Again, I want to try to break this code up for readability.

### Imports

```typescript
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
​
import { ItemData } from '@core/interfaces/multi-select-item-data';
```

Most of these are pretty straight forward ... `ItemData` needs definition ... looking at the interfaces ...

```typescript
export interface ItemData {
  item: string;
  selected: boolean;
}
```

### Component Wrapper

```typescript
@Component({
  selector: 'multiselect-autocomplete',
  templateUrl: './multiselect-autocomplete.component.html',
  styleUrls: ['./multiselect-autocomplete.component.scss']
})
export class MultiselectAutocompleteComponent implements OnInit {
  ...
}
```

### Data Setup

Here are the data points, inputs, and outputs.

```typescript
@Output() result = new EventEmitter<{ key: string, data: Array<string> }>();
​
@Input() placeholder: string = 'Select Data';
@Input() data: Array<string> = [];
@Input() key: string = '';
​
selectControl = new FormControl();
​
rawData: Array<ItemData> = [];
selectData: Array<ItemData> = [];
​
filteredData: Observable<Array<ItemData>>;
filterString: string = '';
```

The `placeholder` and `data` structures are fairly clear. The `key` is passed in, then emitted back out without change. This allows the outside (calling) code to know which object to attach to.

### Initialization

```typescript
constructor() {
  this.filteredData = this.selectControl.valueChanges.pipe(
    startWith<string>(''),
    map(value => typeof value === 'string' ? value : this.filterString),
    map(filter => this.filter(filter))
  );
}

ngOnInit(): void {
  this.data.forEach((item: string) => {
    this.rawData.push({ item, selected: false });
  });
​}
```

Now, I am taking the `data` input and generating matching `rawData` with selected as a boolean.

Additionally, I am binding the `filteredData` to the `selectControl` value changes. This is why we need the `async` in the HTML above.

### Filter and Display Functions

These two functions are used directly on the HTML objects above.

```typescript
filter = (filter: string): Array<ItemData> => {
  this.filterString = filter;
  if (filter.length > 0) {
    return this.rawData.filter(option => {
      return option.item.toLowerCase().indexOf(filter.toLowerCase()) >= 0;
    });
  } else {
    return this.rawData.slice();
  }
};
​
displayFn = (): string => '';
```

### Option Clicked

```typescript
optionClicked = (event: Event, data: ItemData): void => {
  event.stopPropagation();
  this.toggleSelection(data);
};
```

`optionClicked` is named and configured this way for readability.

### Toggle Selection

```typescript
toggleSelection = (data: ItemData): void => {
  data.selected = !data.selected;
​
  if (data.selected === true) {
    this.selectData.push(data);
  } else {
    const i = this.selectData.findIndex(value => value.item === data.item);
    this.selectData.splice(i, 1);
  }
​
  this.selectControl.setValue(this.selectData);
  this.emitAdjustedData();
};
```

`toggleSelection` toggles, adds / removes the value from `selectData`, and emits the changed data.

### Emitting Adjusted Data

```typescript
emitAdjustedData = (): void => {
  const results: Array<string> = []
  this.selectData.forEach((data: ItemData) => {
    results.push(data.item);
  });
  this.result.emit({ key: this.key, data: results });
};
```

Here, I needed to rebuild a simply array of string containing the selected items only.

### Removing a chip

This code seems redundant, but in my mind it was better to describe the functionality clearly.

```typescript
removeChip = (data: ItemData): void => {
  this.toggleSelection(data);
};
```

## Using the Multiselect Autocomplete

### HTML

Here, I passed in the inputs and set a function to capture the emitted `result`.

```html
<multiselect-autocomplete
  [placeholder]="structure[index].subtitle"
  [data]="cardSelects[card.key]"
  [key]="card.key"
  (result)="selectChange($event)">
</multiselect-autocomplete>
```

### TypeScript

Event `key` and `data` are emitted out and used here.

```typescript
selectChange = (event: any) => {
  const key: string = event.key;
  this.cardValue[key] = [ ...event.data ];
};
```

## Code

[Working Example](https://bob-fornal.github.io/multi-select/)
[GitHub Repo](https://github.com/bob-fornal/multi-select/)


## Summary

This was a cool component to create and good challenge. I am pleased with the result, both look-and-feel as well as functionality.