Advanced Angular: Implementing a Reusable Autocomplete Component

Netanel Basal
Netanel Basal
Published in
6 min readApr 9, 2019

--

In this article, I’ll walk you through the process of creating a custom autocomplete component with Angular by using the Angular’s CDK. Along the way we’ll go over various techniques that we can use to build robust components in Angular.

The final result

To see the big picture, here is the general structure detailing the use of the components and directives we’ll describe in this article:

Before we start with the code implementation, we can observe two things from the code above:

First, we can discern that we don’t encapsulate the input text inside the autocomplete component. Instead, we pass the autocomplete reference to the appAutocomplete directive.

The reason for this is that we want to give the component’s consumers the ability to handle the input API directly (for instance in order to define event handlers) without having to expose and manage it from our component.

Secondly, we can see that we don’t couple the OptionComponent selector to our autocomplete component (i.e name it app-autocomplete-option). This is because we want to increase the autocomplete component’s usability by letting us apply the same functionality to other components, such as custom select, custom menus, etc.

Let’s get started.

Creating the OptionComponent

First, we create the app-option component:

In the component’s markup, we wrap the content in a div to apply our style and using ng-content, give the consumer the power to pass any additional required content.

We take the option value via input and expose a click$ observable which we map to this value. We’ll see later how the parent component takes advantage of this property.

Creating the AutocompleteComponent

The HTML structure of our component is pretty minimalistic:

There are a few things to explain here, so let’s break it down:

First, we wrap our content in an ng-template tag to keep it’s instantiation lazy, so it won’t be created until we need it. We obtain a reference to this template by using the ViewChild decorator passing the local variable we named root . Later, we’ll see how we instantiate this template from the AutocompleteDirective.

Within the template we place the autocomplete wrapper div, which is responsible for styling.

Before explaining what we have here, let’s stop for a second and create the AutocompleteContentDirective :

The whole purpose of this directive is to expose a reference to its TemplateRef so its parent, the AutocompleteComponent, can query and render it on demand.

Continuing with the AutocompleteComponent component code, we obtain a reference to the AutocompleteContentDirective that we’ve created, by using the ContentChild decorator. We pass the tpl property which contains the template we want to create to the ngTemplateOutlet directive.

At this point, you might be asking yourself why we’ve used an ng-template wrapper around our OptionComponent, rather than simply placing it in an ng-content, like this:

We opted for that structure due to how ng-content works.

When using ng-content, the host doesn’t have any control over the content. This behavior can lead to unexpected side-effects, as Angular will create the OptionComponent instances without taking into consideration whether they are nested in an autocomplete component.

The way we truly perform the components’ instantiation in a lazy manner is by using ng-template.

Next, we need to query the option child components, and expose an observable that fires upon an option’s click:

We obtain a reference to the OptionComponent children by using the ContentChildren decorator. Next, we create the optionsClick() method which listens to the QueryList’s changes() event that emits the current options, and switches to a new observable which fires each time one of the options is clicked. We’ll see later how the AutocompleteDirective uses this functionality.

A bit of advice: If you have encountered a performance problem you might consider using event delegation.

Lastly, we use the exportAs property as we need to pass the component instance reference to our AutocompleteDirective, as we saw in the beginning of the article. If you’re not familiar with this property, I recommend reading this article.

Creating the AutocompleteDirective

Finally, we reach the last piece of the puzzle:

We have the appAutocomplete input that takes an instance of AppAutoComplete.

In the constructor, we inject a few providers: 1) The host element which in our case is the input element. 2) A reference to the current form control so we can update its value. 3) A reference to the ViewContainerRef and 4) Angular Material’s Overlay service so we can create and position the autocomplete dropdown.

Next, in the ngOnInit hook, we set a focus event listener on the native input element, which calls the openDropdown() method. Let’s see its implementation:

I’m going to be DRY 😀 and redirect you a previous article where I explained in details each step in the code above:

The important part to note here is that we pass this.appAutocomplete.rootTemplate to our overlay instance and that will be the actual template been rendered.

Next, we need to listen to the optionsClick method we’ve created earlier:

When the user clicks on an option, we get the option’s value, set the control’s value with it, and close the dropdown.

Finally, we need to create the functionality of closing the dropdown whenever the user clicks outside:

We listen to the document’s click event, and close the dropdown when a click meets two conditions: 1) The click target isn’t the origin. 2) The click target isn’t the dropdown or any one of its children.

Filtering the Options

The last step on the journey is filtering the options based on the search term. At first glance, you might think to add this functionality to the component itself, but we’ll take a different approach.

As we already have a filter pipe in our application, we’ll prefer composition and use it to filter the options based on the result. This will make our code reusable and also give the consumers the freedom to choose a custom client-side filtering implementation, or even incorporate a server-side based search.

Now, we can use it to filter the result:

We also want to give feedback to the user when there aren’t any results. To do this we’ll refactor our HTML and use a technique described in detail in one of my previous articles, so we don’t need to apply the pipe twice:

The second approach to filtering, that can give us more control, is to use RxJS:

Last but not least, if you want to implement keyboard navigation functionality, check out the following article:

🚀 Have You Tried Akita Yet?

One of the leading state management libraries, Akita has been used in countless production environments. It’s constantly developing and improving.

Whether it’s entities arriving from the server or UI state data, Akita has custom-built stores, powerful tools, and tailor-made plugins, which help you manage the data and negate the need for massive amounts of boilerplate code. We/I highly recommend you try it out.

Follow me on Medium or Twitter to read more about Angular, Akita and JS!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.