Angular custom multiselect control with server search

1. Foreword

Hello everyone 👋, this is my first post all over the internet and today I would like to share with you a solution to a problem I recently encountered at work. It was necessary to implement a multiple select component that suggested items by part of a word by performing a search on the server. In addition, the problem is that from the libraries and UI kits offered to me, I did not find a single suitable solution and I had to write my own implementation. Well, let’s start!

My GitHub Account

2. What do we need

  • Angular 2+.
  • The average level of knowledge (middle level and above) Angular 2+.
  • Create custom controls in Angular I will talk about this a little later, this is not complicated.
  • UI library — Angular Material. Our multiselect with search will be based on some Angular Material components.

3. How to create custom controls in Angular?

If you are not yet familiar with this topic, or want to refresh you are, then please read it.

Angular offers us a simple but powerful tool for creating custom controls, when not enough native ones (input, checkbox, select, etc.).

Another important note, custom controls can be used with both reactive forms and template-driven forms.

For example, we will use the familiar counter component.

First, let’s write the following code:

In order to integrate a custom control into Angular forms, you need this control to implement the interface — ControlValueAccessor. Angular itself uses ControlValueAccessor under the hood to coerce the behavior of native controls.

However, what does Angular do using ControlValueAccessor? It’s simple, it writes the value from the model to the DOM (view), and also raises the control change event to the FormGroup and other directives.

The official documentation for ControlValueAccessor is here — https://angular.io/api/forms/ControlValueAccessor.

As you can see in the official documentation, the interface obliges us to implement three mandatory methods: writeValue, registerOnChange, registerOnTouched and one optional setDisabledState.

writeValue(value: any) — writes a new value to the control. Called when setting the default value new FormControl('Default value') or new value control.setValue('New value').

registerOnChange(fn: any) — Registers a callback function that is called when the value of the control in the view changes, for the (change) method in the view.

registerOnTouched(fn: any) — defines a callback that is called on the event of removing focus from the control (on blur).

setDisabledState(isDisabled: boolean) — (optional to implement) a function that will be called when the [disabled]="true"value changes on the control.

Now that we have a basic knowledge of ControlValueAccessor, let’s apply it to our CounterControlComponent.

In order for the form to know about control changes, we need to call the onChange method for each change in the value in the interface. In order not to write a call to onChange in each method (up and down), in the value setter, we call the onChange method, which will notify the form about updating the value in the control.

To make Angular understand that this is a custom component, we described it in the @Component() decorator:

providers: [{    provide: NG_VALUE_ACCESSOR,    useExisting: forwardRef(() => CounterControlComponent),    multi: true}]

Our custom control is now ready to be used in reactive and template-driven form.

That’s all the magic 🔮, now we can use our custom control with [(ngModel)] in template-driven forms:

<counter-control [(ngModel)]="controlValue"></counter-control>

And also in reactive-driven forms:

<form [formGroup]="form">    <counter-control formControlName="counter"></counter-control></form>

4. Create multiselect-search component

To create our box, we will use the Angular Material Chips component and the Autocomplete component.

This is how our component code will look like:

If you have already worked with Angular Material, then there is not much new for you, but let’s take a closer look at what is happening in the code above.

First, we linked two separate components (Chips and Autocomplete) into one using component composition. This allowed us to enter arbitrary elements into the Chips list, as well as choose from those offered from the Autocomplete component.

Now let’s talk about each component separately:

<mat-chip-list>

It has three important points, they are the event of adding chip to the list, the event of deleting and the event of entering a value in the Input field.

1️⃣ On the chip adding event, we have a method — onAddItem(), which adds an element to <mat-chip-list> if it has not been added yet, saving us from duplication.

2️⃣ For the chip removal event, we have a method — onRemoveItem(), which will remove an item from the list of selected items.

3️⃣ Also, the emitSearchOnTyping() method listens for an event of entering a value in the Input field, and emit a search event if the user stops typing for 300ms debounceTime(300) and the entered value differs from the previous distinctUntilChanged().

<mat-autocomplete>

This component has 2 key points to pay attention to:

1️⃣ This is the event handling of a specific <mat-option> (optionSelected)="onSelectOptionFromSearchResult($event)". The onSelectOptionFromSearchResult() method is almost similar to the onAddItem()method and performs the same function — it adds an item to the selected list.

2️⃣ In our multiselect-search component, we pass the template from drawing option to <mat-autocomplete>:

multiselect-search.component.ts

@Input()
public optionTemplate: TemplateRef<any>;

multiselect-search.component.html

<mat-option
*ngFor="let searchedItem of searchResults
[value]="searchedItem.value"
>
<ng-container
[ngTemplateOutlet]="optionTemplate"
[ngTemplateOutletContext]="{ $implicit: searchedItem.source }"
></ng-container>
</mat-option>

✅ That’s all!

5. Improving the UX of our multiselect-search component

Let’s add to our display component when the search did not find anything and a loading indicator when executing a search request to the server.

Add new <mat-option> for each of these use cases:

For loading indicator

<mat-option *ngIf="loading; else searchResultsOptions" disabled>
<div style="height: 50px;">
<nb-spinner message="Loading..." size="large"></nb-spinner>
</div>
</mat-option>

For empty search result:

<mat-option *ngIf="!loading && searchResults.length === 0 && inputElement?.nativeElement.value.length !== 0" disabled>
<div class="user-message">
<app-user-message message="Nothing found"></app-user-message>
<p class="caption user-message__subtext">
Try to enter a different value for the search, or press Enter if you can confirm the current value.
</p>
</div>
</mat-option>

For search results options:

<ng-template #searchResultsOptions>
<mat-option
*ngFor="let searchedItem of searchResults"
[value]="searchedItem.value"
>
<ng-container
[ngTemplateOutlet]="optionTemplate"
[ngTemplateOutletContext]="{ $implicit: searchedItem.source }"
></ng-container>
</mat-option>
</ng-template>

Full code MultiselectSearchComponent with loading indicator and empty search message:

Now our component can inform the user when the search on the server did not return anything and when the search request is being executed.

6. Example of using multiselect-search component

Thanks for reading, leave your comments and wishes to improve the article. If the article was useful to you put `clap` 👏.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Denis Khrunov

Denis Khrunov

41 Followers

I’m a Front-end Typescript Developer from Russia. I am engaged in front-end development, the main stack of Angular 2+ and in my free time I keep learning.