inocan-group / vue3-google-map

A set of composable components for easy use of Google Maps in your Vue 3 projects.
https://vue3-google-map.com
MIT License
283 stars 57 forks source link

Updating `<Marker :options>` prop doesn't render markers #14

Closed maninak closed 3 years ago

maninak commented 3 years ago

Hello folks,

Is this a bug or am I using the composition API wrong here (I'm new with it)?

I have a BaseMap.vue component which is initially rendered the [] as the value of the prop markers and when the backend responds that prop is value is updated with a (new) populated array.

Here's my BaseMap.vue:

<template>
  <GoogleMap
    id="map"
    ref="googleMapCmp"
    class="base-map"
    map-type-id="hybrid"
    :api-key="GOOGLE_MAPS_CONFIG.key"
    :region="GOOGLE_MAPS_CONFIG.region"
    :language="GOOGLE_MAPS_CONFIG.language"
    :center="center"
    :zoom="zoom"
    :map-type-control="false"
    :clickable-icons="false"
  >
    <Marker
      v-for="marker in markers"
      :key="marker.title"
      :options="{
        title: marker.name,
        position: marker.position,
        clickable: true,
        icon: {
          url: require('../assets/icons/pin.svg'),
          scaledSize: { width: 50, height: 50 },
        },
      }"
      @click="() => centerMapToPosition(marker.position)"
    />
  </GoogleMap>
</template>

<script lang="ts">
import {
  ComponentPublicInstance,
  PropType,
  Ref,
  computed,
  defineComponent,
  ref,
  watch,
} from 'vue'
import { GoogleMap, Marker } from 'vue3-google-map'

import { GOOGLE_MAPS_CONFIG } from '../config'
import { GMapsMarker, GMapsPosition, GPosition } from '../types'
import { getBoundsForMarkers, getOptimalMapZoom } from '../utils'

type MapComponent = ComponentPublicInstance & {
  map: { panTo: (pos: GPosition) => void }
}

export default defineComponent({
  props: {
    center: { type: Object as PropType<GMapsPosition>, required: true },
    markers: { type: Array as PropType<GMapsMarker[]>, default: [] },
    maxZoom: { type: Number, default: 16 },
  },
  components: {
    GoogleMap,
    Marker,
  },
  setup(props) {
    const googleMapCmp = ref(undefined) as Ref<ComponentPublicInstance | undefined>
    const boundedZoom = ref(7)
    const zoom = computed(() => Math.min(props.maxZoom, boundedZoom.value))

    function centerMapToPosition(newCenter: GPosition): void {
      ;(googleMapCmp.value as MapComponent)?.map.panTo(newCenter)
    }

    watch(
      () => props.markers,
      (newMarkers: GMapsMarker[]): void => {
        setTimeout(() => {
          const bounds = getBoundsForMarkers(newMarkers)

          if (bounds) {
            centerMapToPosition(bounds.getCenter())

            const googleMapEl = googleMapCmp.value?.$el as HTMLElement | undefined

            if (googleMapEl) {
              boundedZoom.value = getOptimalMapZoom(
                bounds,
                googleMapEl.getBoundingClientRect(),
              )
            }
          }
        }, 0)
      },
      { immediate: true },
    )

    return {
      googleMapCmp,
      GOOGLE_MAPS_CONFIG,
      center: props.center,
      markers: props.markers,
      zoom,
      centerMapToPosition,
    }
  },
})
</script>

<style lang="scss" scoped>
@import '../styles/constants/_colors.scss';
@import '../styles/utils/_material-shadow';

.base-map {
  @include material-shadow($level: 1, $background: $color-platinum);
  height: 100%;
  border-radius: 4px;

  :deep(.vue-map) {
    border-radius: 4px;
  }
}

#map {
  width: 100%;
  height: 100%;
}
</style>

and the wrapping component StationsMap.vue:

<template>
  <div class="stations-map">
    <BaseMap :center="center" :markers="markers" />
  </div>
</template>

<script lang="ts">
import denormalize from '@weareredlight/denormalize_json_api'
import { TYPE, useToast } from 'vue-toastification'
import { defineComponent } from 'vue'

import BaseMap from '../../../components/BaseMap.vue'
import { fetchFromBackend } from '../../../utils'
import { CurrentLocation, GMapsMarker, GMapsPosition, RESOURCE, Station } from '../../../types'

interface StationWithCurrentLocation extends Station {
  'current-location'?: CurrentLocation
}

interface StationsMapData {
  center: GMapsPosition
  markers: GMapsMarker[]
}

const AUSTRIA_POSITION: GMapsPosition = { lat: 47.6, lng: 14 } as const

export default defineComponent({
  components: { BaseMap },
  data(): StationsMapData {
    return {
      center: AUSTRIA_POSITION,
      markers: [],
    }
  },
  async created(): Promise<void> {
    try {
      const stations: StationWithCurrentLocation[] = denormalize(
        await fetchFromBackend({
          resource: RESOURCE.STATIONS,
          includedResources: [RESOURCE.CURRENT_LOCATION],
        }),
      ).data

      const markers: GMapsMarker[] = []

      stations.forEach((station) => {
        const currentLocation = station['current-location']

        if (currentLocation) {
          markers.push({
            name: station.name,
            position: {
              lat: parseFloat(currentLocation.latitude),
              lng: parseFloat(currentLocation.longitude),
            },
          })
        }
      })

      this.markers = markers
    } catch (error) {
      useToast()(error.message, { type: TYPE.ERROR })
    }
  },
})
</script>

<style lang="scss" scoped>
.stations-map {
  height: 100%;
}
</style>

Expectation:

Actual:

Additional info:

Thanks for all the help!!

HusamElbashir commented 3 years ago

Hi @maninak

Thanks for the detailed issue. Can you provide a minimal reproduction example as well? It's rather difficult to pinpoint the source of the problem from the example you provided.

maninak commented 3 years ago

Hey @HusamIbrahim

I've prepared a code-sandbox for you as requested: https://codesandbox.io/s/adoring-chaplygin-w07rm?file=/src/StationsMap.vue

You may need to hit that reload button once if the gmaps api-key won't work first try

image

On StationsMap.vue, line 28 I have a comment. Whatever is the default prop value is what will be used for the markers, even if it is updated later.

Also notice I'm using vue3-google-map@0.5.1 because of https://github.com/inocan-group/vue3-google-map/issues/13

maninak commented 3 years ago

Hey there, folks! Is there any update on this issue? Anything else I can do to help accelerate it?

If somebody could have a look and help me solve this within the next 1-2 weeks, it would be a MASSIVE help! :pray:

HusamElbashir commented 3 years ago

Apologies @maninak this is still pending. I've had a look at the example you provided but the cause of the issue isn't immediately obvious to me and unfortunately I have little time these days for anything other than a quick fix. If you think you can provide a more minimal example that can highlight the issue more clearly please do share.

HusamElbashir commented 3 years ago

@maninak I've had a second look at the sandbox you provided and I'm not sure if it's an accurate representation of your issue. The non-reactivity in the sandbox is due to the use of a normal function for the setTimeout callback instead of an arrow function. See this fixed version: https://codesandbox.io/s/unruffled-bas-j8ly8

maninak commented 3 years ago

Hey, thank you for looking back into it.

I found the issue and it was on my side! As I suspected I was using the composition API wrong.

In BaseMap.svg (as shown in OP) in setup I'm exposing the props like so, in order to have access to them in the template:

return {
      googleMapCmp,
      GOOGLE_MAPS_CONFIG,
      center: props.center, // <-- oops!
      markers: props.markers, // <-- oops!
      zoom,
      centerMapToPosition,
    }
}

I remember distinctly how this "smelled" back then and I struggled with accepting it, but I also remember that the latest vue3 version at the time seemed to not expose the props to the template automatically (which seemed really weird), unless I returned them from setup.

Soon after I let that issue be, I upgraded vue and found out in other components that I could use the props directly, but totally forgot about refactoring this BaseMap nor that this would fix my problem. Until today! (your comment jolted me into it)

Removing the marked lines fixed my reactivity problem (duh!).

Thank you for your time and sorry for the hassle.

HusamElbashir commented 3 years ago

No worries and glad you could resolve your issue. I'm still learning about the composition API myself so thank you for pointing out that gotcha.