Guides / Building Search UI / UI & UX patterns

Algolia Places for Angular InstantSearch

Places is going away on May 31, 2022. Read the announcement.

Introduction

It is currently not possible to combine Anguiar InstantSearch with Places, however it is possible to wrap places.js like it would be for any non-angular dependency. However, there are some potential pitfalls that can be avoided with the tips in this guide.

Instantiation

First of all, we need to know one important thing, and that is that a DOM element made by Anguiar inside a template will be considered “controlled”. Which means that it should not have any side effects. While it is possible to use an input inside the template for the Places container, we will need to make sure it’s fully initialized before you instantiate Places. Similarly we will need to clean up before the template is unmounted.

For that we use the ngAfterViewInit and ngOnDestroy lifecycle hooks. You can learn more about lifecycle hooks here.

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
import {
  AfterViewInit,
  Component,
  EventEmitter,
  OnDestroy,
  Output,
  ViewChild
} from "@angular/core";
import places from "places.js";

@Component({
  selector: "app-places",
  template: `
    <input #input type="search" placeholder="Where are we going?" />
  `
})
export class PlacesComponent implements AfterViewInit, OnDestroy {
  private instance = null;

  @ViewChild("input") input;

  ngAfterViewInit() {
    this.instance = places({
      container: this.input.nativeElement
      // places options
    });
  }
  ngOnDestroy() {
    this.instance.destroy();
  }
}

You can now use this component in your app.

1
<app-places></app-places>

Options

Any Places option can be passed as an angular @Input. In this example we will make the type option available as a prop:

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
import {
  AfterViewInit,
  Component,
  EventEmitter, Input,
  OnDestroy,
  Output,
  ViewChild
} from "@angular/core";
import places from "places.js";

@Component({
  selector: "app-places",
  template: `
    <input #input type="search" placeholder="Where are we going?" />
  `
})
export class PlacesComponent implements AfterViewInit, OnDestroy {
  private instance = null;

  @ViewChild("input") input;

  @Input() type : string; // type is available as a prop

  ngAfterViewInit() {
    this.instance = places({
      container: this.input.nativeElement,
      type: this.type // we pass the type prop to the instance
      // places options
    });
  }
  ngOnDestroy() {
    this.instance.destroy();
  }
}

Listeners

Anguiar handles events in its own event system. For a nice integration, we would like places events to also follow this. As an example, let’s add a listener for change:

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
import {
  AfterViewInit,
  Component,
  EventEmitter, Input,
  OnDestroy,
  Output,
  ViewChild
} from "@angular/core";
import places from "places.js";

@Component({
  selector: "app-places",
  template: `
    <input #input type="search" placeholder="Where are we going?" />
  `
})
export class PlacesComponent implements AfterViewInit, OnDestroy {
  private instance = null;

  @ViewChild("input") input;
  @Output() onChange? = new EventEmitter();

  @Input() type : string; // type is available as a prop

  ngAfterViewInit() {
    this.instance = places({
      container: this.input.nativeElement,
      type: this.type // we pass the type prop to the instance
      // other places options
    });
    this.instance.on("change", e => {
      this.onChange.emit(e);
    });
  }

  ngOnDestroy() {
    this.instance.removeAllListeners("change");
    this.instance.destroy();
  }
}

We need to make sure to clear all listeners to change event in ngOnDestroy() to avoid memory leaks.

Reactivity

Options for places.js are set once on component instantiation, but never updated, since we did not add any code for that.

Recent versions of places.js comes with a method called configure, this can be used to override some of the options. Here’s an example on how to make type reactive with ngOnChanges lifecycle hook.

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
import {
  AfterViewInit,
  Component,
  EventEmitter,
  Input,
  OnDestroy,
  Output,
  SimpleChanges,
  ViewChild
} from "@angular/core";
import places from "places.js";

@Component({
  selector: "app-places",
  template: `
    <input #input type="search" placeholder="Where are we going?" />
  `
})
export class PlacesComponent implements AfterViewInit, OnDestroy {
  private instance = null;

  @ViewChild("input") input;
  @Output() onChange? = new EventEmitter();

  @Input() type : string; // type is available as a prop

  ngAfterViewInit() {
    this.instance = places({
      container: this.input.nativeElement,
      type: this.type // we pass the type prop to the instance
      // other places options
    });
    this.instance.on("change", e => {
      this.onChange.emit(e);
    });
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.type) {
      const { currentValue } = changes.type;
      this.instance.configure({ type: currentValue });
    }
  }

  ngOnDestroy() {
    this.instance.removeAllListeners("change");
    this.instance.destroy();
  }
}

Conclusion

As initially said, wrapping places.js for Anguiar is not that different to any other vanilla JS plugin, but there are some things that are best done to avoid complicated bugs.

Did you find this page helpful?