Guides / Building Search UI / UI & UX patterns

Introduction

It is currently not possible to combine Vue InstantSearch with places, however it is possible to wrap places.js like it would be for any non-vue 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 Vue inside a template will be considered “controlled”. Which means that it should not have any side effects. While it’s possible to use an input inside the template for the places container, it will not be able to be unmounted. For that reason we will make an input with the DOM ourselves inside the mounted 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
<!-- components/Places.vue -->
<template>
  <!-- container for places.js -->
  <div />
</template>

<script>
import places from 'places.js';

export default {
  data() {
    return { instance: null };
  },
  mounted() {
    // make sure Vue does not know about the input
    // this way it can properly unmount
    this.input = document.createElement('input');
    this.$el.appendChild(this.input);

    this.instance = places({
      container: this.input,
    });
  },
  beforeDestroy() {
    // if you had any "this.instance.on", also call "off" here
    this.instance.destroy();
  },
};
</script>

Options

Any places option can be passed as a prop to Vue. 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
<script>
import places from 'places.js';

export default {
  props: {
    type: {
      type: String,
      required: false,
    },
  },
  data() {
    return { instance: null };
  },
  mounted() {
    // make sure Vue does not know about the input
    // this way it can properly unmount
    this.input = document.createElement('input');
    this.$el.appendChild(this.input);

    this.instance = places({
      type: this.type,
      container: this.input,
    });
  },
};
</script>

Listeners

Vue 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
<script>
import places from 'places.js';

export default {
  data() {
    return { instance: null };
  },
  mounted() {
    // make sure Vue does not know about the input
    // this way it can properly unmount
    this.input = document.createElement('input');
    this.$el.appendChild(this.input);

    this.instance = places({
      apiKey: this.apiKey,
      appId: this.appId,
      type: this.type,
      container: this.input,
    });

    this.instance.on('change', e => {
      this.$emit('change', e);
    });
  },
  beforeDestroy() {
    this.instance.off('change');
    this.instance.destroy();
  },
};
</script>

We can then listen from the container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
  <div>
    <app-places @change="suggestion = $event.suggestion" />
    <pre>{{ suggestion }}</pre>
  </div>
</template>
<script>
import AppPlaces from './components/Places';

export default {
  components: { AppPlaces },
  data() {
    return {
      suggestion: undefined,
    };
  },
};
</script>

Reactivity

Options for places.js are set once on component instantiation, but never updated, since we didn’t add any code for that. We have two options to solve this shortcoming if necessary:

  1. watching for change in the props

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.

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
<script>
import places from 'places.js';

export default {
  props: {
    type: {
      type: String,
      required: false,
    },
  },
  data() {
    return { instance: null };
  },
  mounted() {
    // make sure Vue does not know about the input
    // this way it can properly unmount
    this.input = document.createElement('input');
    this.$el.appendChild(this.input);

    this.instance = places({
      type: this.type,
      container: this.input,
    });
  },
  watch: {
    type(newVal) {
      this.instance.configure({
        type: newVal,
      });
    },
  },
};
</script>

However, not every prop / option can be updated with this method. For those you will need:

  1. adding a key to the component in case of change

The easiest way to update configuration is to make a new instance of the component. You can force Vue to go through the component life cycle again and make a new DOM element, by giving a new :key to the places component. The value of this key should be unique for the prop we want to change. The easiest way is to use this value as the key itself. Here demonstrated by apiKey and appId:

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
<template>
  <div>
    <app-places
      :api-key="apiKey"
      :app-id="appId"
      :key="`${appId}-${apiKey}`"
    />
    <button @click="toggleApiKey">
      Toggle <code>apiKey</code> ({{ apiKey }} - {{ appId }})
    </button>
  </div>
</template>

<script>
import AppPlaces from './components/Places';

export default {
  components: { AppPlaces },
  data() {
    return {
      appId: undefined,
      apiKey: undefined,
    };
  },
  methods: {
    toggleApiKey() {
      if (this.apiKey) {
        this.apiKey = undefined;
        this.appId = undefined;
      } else {
        this.apiKey = 'your-api-key';
        this.appId = 'your-appId-key';
      }
    },
  },
};
</script>

Conclusion

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

You can find the complete source code of the example on GitHub.

Did you find this page helpful?