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
264 stars 50 forks source link

Add reactivity to language and region props #178

Closed JoseGoncalves closed 5 months ago

JoseGoncalves commented 8 months ago

While Google Maps does not provide a way to dynamicaly change the interface language, the only way to do it is by unloading/loading the map API. This PR does this when language and region props are changed.

JoseGoncalves commented 8 months ago

As I needed to test this change on a live app, I've published it as @josempgon/vue3-google-map.

HusamElbashir commented 8 months ago

I would prefer not to have to maintain code related to removing the google API script. This is better handled at the API script loader level. @googlemaps/js-api-loader has some long standing issues related to this (e.g. https://github.com/googlemaps/js-api-loader/issues/100). We could alternatively switch to another library if it could handle that. In the meantime we have an API that allows loading the script externally so the user could write a wrapper component for GoogleMap that handles loading/reloading the script: https://github.com/inocan-group/vue3-google-map/issues/99#issuecomment-1237395931

JoseGoncalves commented 8 months ago

Hi @HusamIbrahim, I have done it firstly using the apiPromise prop and the @googlemaps/js-api-loader package to be able to unload/load the maps API, but I find it somehow cumbersome to do that. Someone that uses a Vue component that has a language prop would expect that, when that prop is changed, the language is changed in the interface, without needing to know what is done internaly in the library to handle that change.

HusamElbashir commented 8 months ago

I agree it is cumbersome but like I said I'd rather not maintain code like this, especially considering the use case is not that common. This is better handled at the loader lib level so I'm happy to migrate if there is an alternative that handles this. Otherwise we do have apiPromise as a workaround, cumbersome as it may be. On managing user expectations we could add to the docs that these props are non-reactive.

JoseGoncalves commented 8 months ago

I understand you wouldn't like to maintain this kind of functionality, just don't agree with you as it's an unusual use case. At least in non-english speaking countries, it's usual to have to design multilingual apps and, if that app contains a map, we would expect the map language is in sync with the app language.

At least it would be nice to have documentation on how to implement the language switch with the available API.

HusamElbashir commented 8 months ago

No harm in adding that to the docs. I'm happy to accept another PR if you'd like to share the recipe you used here.

JoseGoncalves commented 8 months ago

Hi @HusamIbrahim. I've managed to do it with a wrapper component for GoogleMap like this:

<script setup>
import { nextTick, ref, watch } from 'vue';
import { GoogleMap } from 'vue3-google-map';
import { Loader } from '@googlemaps/js-api-loader';
import { useState } from '@/composables/state';

const props = defineProps({
    apiKey: {
        type: String,
        default: ''
    },
    version: {
        type: String,
        default: 'weekly'
    },
    region: {
        type: String,
        default: undefined
    },
    language: {
        type: String,
        default: undefined
    },
    libraries: {
        type: Array,
        default: () => ['places']
    }
});

const emit = defineEmits(['map-ready']);

const { mapsLoader } = useState();
const apiPromise = ref(null);
const mapRef = ref(null);

const loadMaps = () => {
    const { apiKey, version, region, language, libraries } = props;
    mapsLoader.value = new Loader({
        apiKey,
        version,
        region,
        language,
        libraries
    });
    apiPromise.value = mapsLoader.value.load();
};

const unloadMaps = () => {
    const nodes = document.querySelectorAll(
        'script[src*="maps.googleapis.com"], link[href*="fonts.googleapis.com"]'
    );
    nodes.forEach(el => {
        if (el.parentNode) el.parentNode.removeChild(el);
    });
    delete window.google.maps;
    Loader.instance = null;
};

watch(
    () => mapRef.value?.ready,
    ready => {
        if (ready) emit('map-ready', mapRef.value);
    }
);

watch(
    () => [props.region, props.language],
    async () => {
        mapsLoader.value = null;
        await nextTick();
        unloadMaps();
        loadMaps();
    }
);

if (mapsLoader.value) {
    const { language, region } = mapsLoader.value.options;
    if (language !== props.language || region !== props.region) {
        unloadMaps();
        loadMaps();
    } else {
        apiPromise.value = mapsLoader.value.load();
    }
} else {
    loadMaps();
}
</script>

<template>
    <GoogleMap
        v-if="mapsLoader"
        ref="mapRef"
        :api-promise="apiPromise"
    >
        <slot />
    </GoogleMap>
</template>

What do you think of this approach?

The useState() composable used to store the map loader instance was implemented with the createGlobalState function from the VueUse library. Do you think that composable should be added also to the docs?

HusamElbashir commented 8 months ago

Yeah that might be a bit too long for the docs. Maybe you could create an example in https://vite.new/vue and just link to it. Make sure it's frozen so it doesn't get updated.

JoseGoncalves commented 8 months ago

Being a bit long was why I've tried to push that functionality inside to the main GoogleMap component.

Sorry, I currently don't have time to setup an example in https://vite.new/vue.

HusamElbashir commented 5 months ago

Being a bit long was why I've tried to push that functionality inside to the main GoogleMap component.

Sorry, I currently don't have time to setup an example in https://vite.new/vue.

That's perfectly fine we can always point people to your example here if they ever need this.