Guides / Building Search UI / UI & UX patterns

Geo search with Angular InstantSearch

Introduction

We will see how we can leverage the geo search capabilities of Algolia with the connectGeoSearch connector. This demo is built with Google Maps but the core logic is not tied to any map provider. You can build your own map widget with a different provider (e.g. Leaflet).

Since wrapping Google Maps for Angular isn’t trivial, this guide uses the Angular Google Maps component to save time.

Displaying hits

First of all, let’s add a GeoSearchComponent extending BaseWidget and using connectGeoSearch.

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
import { Component, Inject, forwardRef, Optional } from '@angular/core';
import {
  TypedBaseWidget,
  NgAisInstantSearch,
  NgAisIndex,
} from 'angular-instantsearch';
import connectGeoSearch, {
  GeoSearchConnectorParams,
  GeoSearchWidgetDescription,
} from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch';

@Component({
  selector: 'ais-geo-search',
  template: `It works!`,
})
export class GeoSearchComponent extends TypedBaseWidget<
  GeoSearchWidgetDescription,
  GeoSearchConnectorParams
> {
  constructor(
    @Inject(forwardRef(() => NgAisIndex))
    @Optional()
    public parentIndex: NgAisIndex,
    @Inject(forwardRef(() => NgAisInstantSearch))
    public instantSearchInstance: NgAisInstantSearch
  ) {
    super('GeoSearch');
  }

  public state: GeoSearchWidgetDescription['renderState'] = {
    items: [],
  } as any;

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

  get center() {
    if (this.state.items && this.state.items.length > 0) {
      const [first] = this.state.items;
      return first._geoloc || { lat: 0, lng: 0 };
    }
    return { lat: 0, lng: 0 };
  }
}

Now, you can introduce the Angular Google Maps component inside your app, along with the newly created GeoSearchComponent.

$
$
npm install --save @angular/google-maps
yarn add @angular/google-maps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mport { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgAisModule } from 'angular-instantsearch';

import { AppComponent } from './app.component';
import { GoogleMapsModule } from '@angular/google-maps';
import { GeoSearchComponent } from './geo-search.component';

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

In your index.html, inject the Google Maps library and fix your Google Maps API key.

1
2
3
4
5
6
<!doctype html>
<html lang="en">
<head>
  <!-- ... -->
  <script src="https://maps.googleapis.com/maps/api/js?key=AIxxxxx"></script>
</head>

Now that you have access to the data from the Geo Search connector, you can add a regular map in the template.

1
2
<google-map [center]="center" [zoom]="7" width="100%">
</google-map>

To provide the center, you need to add the following getting to your component, computed off the results returned to the state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class GeoSearchComponent extends TypedBaseWidget<
  GeoSearchWidgetDescription,
  GeoSearchConnectorParams
> {

  // ...
  get center() {
    if (this.state.items && this.state.items.length > 0) {
      const [first] = this.state.items;
      return first._geoloc || { lat: 0, lng: 0 };
    }
    return { lat: 0, lng: 0 };
  }
}

The final step is looping over this.state.items to display the hits from this search. Let’s modify our template to do this:

1
2
3
4
5
6
<google-map [center]="center" [zoom]="7" width="100%">
  <map-marker
    *ngFor="let item of state.items"
    [position]="item._geoloc"
  ></map-marker>
</google-map>

That’s it. Now you should have a complete infinite scroll experience. Don’t forget that the complete source code of the example is available on GitHub.

Full example

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
import { Component, Inject, forwardRef, Optional } from '@angular/core';
import {
  TypedBaseWidget,
  NgAisInstantSearch,
  NgAisIndex,
} from 'angular-instantsearch';
import connectGeoSearch, {
  GeoSearchConnectorParams,
  GeoSearchWidgetDescription,
} from 'instantsearch.js/es/connectors/geo-search/connectGeoSearch';

@Component({
  selector: 'ais-geo-search',
  template: `
    <google-map [center]="center" [zoom]="7" width="100%">
      <map-marker
        *ngFor="let item of state.items"
        [position]="item._geoloc"
      ></map-marker>
    </google-map>
  `,
})
export class GeoSearchComponent extends TypedBaseWidget<
  GeoSearchWidgetDescription,
  GeoSearchConnectorParams
> {
  constructor(
    @Inject(forwardRef(() => NgAisIndex))
    @Optional()
    public parentIndex: NgAisIndex,
    @Inject(forwardRef(() => NgAisInstantSearch))
    public instantSearchInstance: NgAisInstantSearch
  ) {
    super('GeoSearch');
  }

  public state: GeoSearchWidgetDescription['renderState'] = {
    items: [],
  } as any;

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

  get center() {
    if (this.state.items && this.state.items.length > 0) {
      const [first] = this.state.items;
      return first._geoloc || { lat: 0, lng: 0 };
    }
    return { lat: 0, lng: 0 };
  }
}

Going further

This guide only explains how to display hits on a map, but connectGeoSearch connector has more features, like refining the search when the map moves, automatically centering on the correct items etc. Feel free to explore the options given to state from the connector to make these experiences.

Did you find this page helpful?