Concepts / Building Search UI / Autocomplete
Aug. 21, 2019

Autocomplete

You are reading the documentation for Angular InstantSearch v3, which is in beta. You can find the v2 documentation here.

Overview

A common pattern in search is to implement a search box with an autocomplete as a first step of the search experience. Angular InstantSearch doesn’t come with a built-in widget for the autocomplete. But you can create your own using the autocomplete connector on a generic autocomplete component such as Autocomplete.

In this guide we will cover a search box which displays an autocomplete menu linked to a results page.

We won’t cover the usage of the connector in a multi-index context in this guide. There is a dedicated section about that in the multi-index search guide. You can find the source code of both examples on GitHub.

Results page with autocomplete

To implement this, we use the component Autocomplete provided by Angular Material. Install and import it.

$
ng add @angular/material
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app.module.ts
import { NgAisModule } from 'angular-instantsearch';
import { MatInputModule, MatAutocompleteModule } from '@angular/material/autocomplete';
// make sure you have BrowserAnimationsModule
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';

@NgModule({
 declarations: [
   AppComponent,
 ],
 imports: [
   BrowserModule, BrowserAnimationsModule, NgAisModule.forRoot(),
   MatInputModule, MatAutocompleteModule,
 ],
 providers: [],
 bootstrap: [AppComponent],
})
export class AppModule {}

The next step is to create a custom Angular InstantSearch component AutocompleteComponent.

When connected to autocomplete, three interesting props are exposed in the internal state:

  • query: the query string entered by the user,
  • indices: the list of suggestions,
  • refine: a function that takes a query string and retrieves relevant suggestions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { Component, Inject, forwardRef, Input, Output, EventEmitter } from '@angular/core';
import { BaseWidget, NgAisInstantSearch } from 'angular-instantsearch';
import { connectAutocomplete } from 'instantsearch.js/es/connectors';

@Component({
  selector: 'app-autocomplete',
  template: `
    <div>
      <input
        matInput
        [matAutocomplete]="auto"
        (keyup)="handleChange($event)"
        style="width: 100%; padding: 10px"
      />
      <mat-autocomplete
        #auto="matAutocomplete"
        style="margin-top: 30px; max-height: 600px"
      >
        <div *ngFor="let index of state.indices || []">
          <mat-option
            *ngFor="let option of index.hits"
            [value]="option.name"
            (click)="onQuerySuggestionClick.emit({ query: option.name })"
          >
            {{ option.name }}
          </mat-option>
        </div>
      </mat-autocomplete>
    </div>
  `
})
export class AutocompleteComponent extends BaseWidget {
  state: {
    query: string;
    refine: Function;
    indices: object[];
  };

  @Output() onQuerySuggestionClick = new EventEmitter<{ query: string }>();

  constructor(
    @Inject(forwardRef(() => NgAisInstantSearch))
    public instantSearchParent
  ) {
    super('AutocompleteComponent');
  }

  public handleChange($event: KeyboardEvent) {
    this.state.refine(($event.target as HTMLInputElement).value);
  }

  public ngOnInit() {
    this.createWidget(connectAutocomplete, {});
    super.ngOnInit();
  }
}

When we have our autocomplete component set up we can integrate it in our application. But for that we need to use two instances of the InstantSearch component. We use two instances because it allows us to configure the number of hits retrieved by the autocomplete differently than the number of results. The huge benefit of it is also to not have the query tied to both instances at the same time. It allows us to clear the suggestions but sill have the query applied on the second instance. Since we have two instances we need a way to sync the query between the two. We use the ais-configure to achieve this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Component } from '@angular/core';
import * as algoliasearch from 'algoliasearch/lite';

const searchClient = algoliasearch(
  'YourApplicationID',
  'YourSearchOnlyAPIKey'
);

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  config = {
    indexName: 'demo_ecommerce',
    searchClient
  };

  public searchParameters = {
    query: ''
  };

  public setQuery({ query }: { query: string }) {
    this.searchParameters.query = query;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="ais-InstantSearch">
  <ais-instantsearch [config]="config">
    <div class="searchbox">
      <app-autocomplete (onQuerySuggestionClick)="setQuery($event)"></app-autocomplete>
    </div>
  </ais-instantsearch>
  <ais-instantsearch [config]="config">
    <div class="left-panel">
      <ais-current-refinements></ais-current-refinements>
      <h2>Brands</h2>
      <ais-refinement-list attribute="brand"></ais-refinement-list>
      <ais-configure [searchParameters]="{ hitsPerPage: 8 }"></ais-configure>
    </div>
    <div class="right-panel">
      <ais-configure [searchParameters]="searchParameters"></ais-configure>
      <ais-hits>
        <ng-template let-hits="hits">
          <ol class="ais-Hits-list">
            <li *ngFor="let hit of hits" class="ais-Hits-item">
              <!-- ... -->
            </li>
          </ol>
        </ng-template>
      </ais-hits>
      <ais-pagination></ais-pagination>
    </div>
  </ais-instantsearch>
</div>

That’s it! You can find the complete example on GitHub.

Did you find this page helpful?