Search code examples
google-mapsnuxt.jsalgolia

Nuxt + Algolia - Google Maps not updating the new results from instantsearch


I am developing an instantsearch using nuxt. Googlemaps receives the first set on results on page load and renders the markers without any issues - however when searching the new results do not update on the map? Is there a way i can push the new set of results through to the map to render the markers?

I assume this will be the beforeMount hook and pass the results to updateMap?

pages/index.vue

<template>
  <div class="root">

    <div class="container">

      <ais-instant-search-ssr
        :search-client="searchClient"
        index-name="dive_buddy"
      >
        <ais-configure :hits-per-page.camel="7">
          <ais-search-box placeholder="Search here…" class="searchbox" />
          <ais-stats />

          <div class="h-screen">
            <ais-hits>
              <template #default="{ items }">
                <div class="grid grid-cols-2 gap-2">
                  <div v-if="items.length">
                    <div
                      v-for="item in items"
                      :key="item.objectID"
                      class="my-2 p-2 border"
                    >
                      <SiteRow
                        :site="item"
                        @mouseover.native="highlightMarker(item.objectID, true)"
                        @mouseout.native="highlightMarker(item.objectID, false)"
                      />
                    </div>
                  </div>
                  <div v-else>No results found</div>
                  <Map
                    :items="items"
                    :lat="items[0].lat"
                    :lng="items[0].lng"
                  />
                </div>
              </template>
            </ais-hits>
            <ais-pagination />
          </div>
        </ais-configure>
      </ais-instant-search-ssr>
    </div>
  </div>
</template>

<script>
import {
  AisInstantSearchSsr,
  AisConfigure,
  AisHits,
  AisSearchBox,
  AisStats,
  AisPagination,
  createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'

function renderToString(app) {
  return new Promise((resolve, reject) => {
    _renderToString(app, (err, res) => {
      if (err) reject(err)
      resolve(res)
    })
  })
}

const searchClient = algoliasearch(
  'xxx', // AppID
  'xxxxxxxx' // APIKey
)

export default {
  components: {
    AisInstantSearchSsr,
    AisConfigure,
    AisHits,
    AisSearchBox,
    AisStats,
    AisPagination,
  },
  mixins: [
    createServerRootMixin({
      searchClient,
      indexName: 'dive_buddy',
    }),
  ],
  data() {
    return {
      searchClient,
    }
  },
  head() {
    return {
      link: [
        {
          rel: 'stylesheet',
          href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
        },
      ],
    }
  },

  serverPrefetch() {
    return this.instantsearch
      .findResultsState({
        component: this,
        renderToString,
      })
      .then((algoliaState) => {
        this.$ssrContext.nuxt.algoliaState = algoliaState
      })
  },

  beforeMount() {
    const results =
      (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
      window.__NUXT__.algoliaState

    this.instantsearch.hydrate(results)

    // Remove the SSR state so it can't be applied again by mistake
    delete this.$nuxt.context.nuxtState.algoliaState
    delete window.__NUXT__.algoliaState
  },

  methods: {
    highlightMarker(id, isHighlighted) {
      document
        .getElementsByClassName(`id-${id}`)[0]
        ?.classList?.toggle('marker-highlight', isHighlighted)
    },
  },
}
</script>

components/Map.vue

<template>
  <div ref="map" class="h-screen"></div>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true,
    },
    lat: {
      type: Number,
      required: true,
    },
    lng: {
      type: Number,
      required: true,
    },
  },

  mounted() {
    this.updateMap()
  },

  methods: {
    // run showMap function in maps.client.js
    async updateMap() {
      await this.$maps.showMap(
        this.$refs.map,
        this.lat,
        this.lng,
        this.getMarkers()
      )
    },

    // pass through markers to googlemaps
    getMarkers() {
      return this.items.map((item) => {
        return {
          ...item,
        }
      })
    },
  },
}
</script>

plugins/maps.client.js

export default function(context, inject){
    let isLoaded = false
    let waiting = []
    
    addScript()
    inject('maps', {
        showMap,
    })


    function addScript(){
        const script = document.createElement('script')
        script.src = "https://maps.googleapis.com/maps/api/js?key=xxxx&libraries=places&callback=initGoogleMaps"
        script.async = true
        window.initGoogleMaps = initGoogleMaps
        document.head.appendChild(script)
    }

    function initGoogleMaps(){
        isLoaded =  true
        waiting.forEach((item) => {
            if(typeof item.fn === 'function'){
                item.fn(...item.arguments)
            }
        })
        waiting = []
    }

    function showMap(canvas, lat, lng, markers){
        if(!isLoaded){
            waiting.push({
                fn: showMap,
                arguments,
            })
            return
        }
        const mapOptions = {
            zoom: 18,
            center: new window.google.maps.LatLng(lat, lng),
            disableDefaultUI: true,
            zoomControl: true,
            styles:[{
                featureType: 'poi.business',
                elementType: 'labels.icon',
                stylers:[{ visibility: 'off' }]
            }]
        }
        const map = new window.google.maps.Map(canvas, mapOptions)
        if(!markers){
            const position = new window.google.maps.LatLng(lat, lng)
            const marker = new window.google.maps.Marker({ 
                position,
                clickable: false,
            })
            marker.setMap(map)
            return
        }

        const bounds = new window.google.maps.LatLngBounds()
        let index = 1
        markers.forEach((item) => {
            const position = new window.google.maps.LatLng(item.lat, item.lng)
            const marker = new window.google.maps.Marker({ 
                position, 
                label: {
                    text: `${index}`,
                    color: 'black',
                    className: `marker id-${item.objectID}`
                },
                icon: 'https://maps.gstatic.com/mapfiles/transparent.png',
                clickable: false,
            })
            marker.setMap(map)
            bounds.extend(position)
            index++
        })

        map.fitBounds(bounds)
        
    }
}

enter image description here

EDIT As per @Bryan's suggestion - I have previously tried to follow algolia's documentation using vue-googlemaps plugin but this does not work on Nuxt as the map does not load as well as these errors -
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'g')
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'h')


Solution

  • Although this isnt a solution to the above - I have found a workaround using a different library: vue2-google-maps as this works with Nuxt (see docs).

    pages/index.vue

    <template>
      <div class="root">
    
        <div class="container">
          <ais-instant-search-ssr
            :search-client="searchClient"
            index-name="dive_centres"
          >
            <ais-configure :hits-per-page.camel="7">
              <ais-search-box placeholder="Search here…" class="searchbox" />
              <ais-stats />
    
              <div class="h-screen">
                <ais-hits>
                  <template #default="{ items }">
                    <div class="grid grid-cols-2 gap-2">
                      <div v-if="items.length">
                        <div
                          v-for="item in items"
                          :key="item.objectID"
                          class="my-2 p-2 border"
                        >
                          <SiteRow
                            :site="item"
                            @mouseover.native="highlightMarker(item.objectID, true)"
                            @mouseout.native="highlightMarker(item.objectID, false)"
                          />
                        </div>
                      </div>
                      <div v-else>No results found</div>
                      <Map />
                    </div>
                  </template>
                </ais-hits>
                <ais-pagination />
              </div>
            </ais-configure>
          </ais-instant-search-ssr>
        </div>
      </div>
    </template>
    
    <script>
    import {
      AisInstantSearchSsr,
      AisConfigure,
      AisHits,
      AisSearchBox,
      AisStats,
      AisPagination,
      createServerRootMixin,
    } from 'vue-instantsearch'
    import algoliasearch from 'algoliasearch/lite'
    import _renderToString from 'vue-server-renderer/basic'
    
    function renderToString(app) {
      return new Promise((resolve, reject) => {
        _renderToString(app, (err, res) => {
          if (err) reject(err)
          resolve(res)
        })
      })
    }
    
    const searchClient = algoliasearch(
      'xxx',     // AppID
      'xxxxxxxx' // APIKey
    )
    
    export default {
      components: {
        AisInstantSearchSsr,
        AisConfigure,
        AisHits,
        AisSearchBox,
        AisStats,
        AisPagination,
      },
      mixins: [
        createServerRootMixin({
          searchClient,
          indexName: 'dive_sites',
        }),
      ],
    
      head() {
        return {
          link: [
            {
              rel: 'stylesheet',
              href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
            },
          ],
        }
      },
    
      serverPrefetch() {
        return this.instantsearch
          .findResultsState({
            component: this,
            renderToString,
          })
          .then((algoliaState) => {
            this.$ssrContext.nuxt.algoliaState = algoliaState
          })
      },
    
      beforeMount() {
        const results =
          (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
          window.__NUXT__.algoliaState
    
        this.instantsearch.hydrate(results)
    
        // Remove the SSR state so it can't be applied again by mistake
        delete this.$nuxt.context.nuxtState.algoliaState
        delete window.__NUXT__.algoliaState
      },
    
      methods: {
        highlightMarker(id, isHighlighted) {
          console.log(id)
          document
            .getElementsByClassName(`id-${id}`)[0]
            ?.classList?.toggle('marker-highlight', isHighlighted)
        },
      },
    }
    </script>
    

    components/Map.vue

    <template>
      <div v-if="state" class="ais-GeoSearch">
        
        <GmapMap
          :center="center"
          :zoom="zoom"
          map-type-id="terrain"
          class="h-full w-full"
          :options="options"
        >
          <GmapMarker
            v-for="item in state.items"
            :key="item.objectID"
            :position="item._geoloc"
            :clickable="false"
            :draggable="true"
            :icon="{ url: require('~/static/images/dive-site-small.svg') }"
          />
        </GmapMap>
      </div>
    </template>
    
    <script>
    import { createWidgetMixin } from 'vue-instantsearch'
    import { connectGeoSearch } from 'instantsearch.js/es/connectors'
    
    export default {
      mixins: [createWidgetMixin({ connector: connectGeoSearch })],
    
      data() {
        return {
          zoom: 4,
          options: {
            disableDefaultUI: true,
          },
        }
      },
      computed: {
        center() {
          return this.state.items.length !== 0
            ? this.state.items[0]._geoloc
            : { lat: 48.864716, lng: 2.349014 }
        },
      },
    }
    </script>