Search code examples
vue.jsvuejs3mapbox-gl-jsv-model

how to update the modelValue property when you have a reactive object in Vue 3?


I am new to Vue and my understanding of v-model is still limited. Now. I am trying to make a Vue 3 composable version of the example provided in https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-vue/. The problem I have is that the props.modelValue does not seem to update. Would you know what is wrong in the the parent/child .vue ?

Here is the parent App.vue:

<script setup>
  import Map from './components/Map.vue';
  import '../node_modules/mapbox-gl/dist/mapbox-gl.css';
  import { ref, reactive, defineModel } from 'vue';

  const model = defineModel()
  const location = reactive(
    { lng: -71.224518, 
      lat: 42.213995, 
      bearing: 0, 
      pitch: 0, 
      zoom: 9 },
  )

</script>

<template>
  <div id="layout">
    <div id="sidebar">
      Longitude: {{ location.lng.toFixed(4) }} | Latitude: {{
      location.lat.toFixed(4) }} | Zoom: {{ location.zoom.toFixed(2) }} |
      <template v-if="location.bearing">
        Bearing: {{ location.bearing.toFixed(2) }} |
      </template>
      <template v-if="location.pitch">
        Pitch: {{ location.pitch.toFixed(2) }} |
      </template>
      <button
        @click="location = { lng: -71.224518, lat: 42.213995, zoom: 9, pitch: 0, bearing: 0 }"
      >
        Reset
      </button>
    </div>
    <Map v-model="location" />
  </div>
</template>

<style>
  #layout {
    flex: 1;
    display: flex;
  }

  #sidebar {
    background-color: rgb(255 255 0 / 50%);
    color: #fff;
    padding: 6px 12px;
    font-family: monospace;
    font-weight: bold;
    z-index: 1;
    position: absolute;
    top: 0;
    left: 0;
    margin: 12px;
    border-radius: 4px;
  }
</style>

Here is the child Map.vue:

<script setup>
import mapboxgl from 'mapbox-gl';
import { ref, watch, onMounted, onUnmounted } from 'vue'
mapboxgl.accessToken =  <UserAccessToken />; // here you need your mapbox access token

const props = defineProps({
    modelValue:
    {
        type: Object,
        required: true
    }
});

const emit = defineEmits(['update:modelValue']);

const mapContainer = ref(null);
const thismap = ref(null);

onMounted(() => {
    const { lng, lat, zoom, bearing, pitch } = props.modelValue;
    const map = new mapboxgl.Map({
        container: mapContainer.value,
        style: "mapbox://styles/mapbox/dark-v11", //'mapbox://styles/mapbox/streets-v12',
        center: [lng, lat],
        bearing,
        pitch,
        zoom
    });

    const updateLocation = () => {
        emit('update:modelValue', getLocation()); // here I think it is the issue
    }

    map.on('move', updateLocation);
    map.on('zoom', updateLocation);
    map.on('rotate', updateLocation);
    map.on('pitch', updateLocation);
    
    thismap.value = map;
    
    
});

onUnmounted(() => {
    thismap.value.remove();
    thismap = null;
    });


watch (() => props.modelValue, (next) => {
    const curr = getLocation();
    const map = thismap.value;
    if (curr.lng != next.lng || curr.lat != next.lat)
    thismap.setCenter({ lng: next.lng, lat: next.lat });
    if (curr.pitch != next.pitch) thismap.setPitch(next.pitch);
    if (curr.bearing != next.bearing) thismap.setBearing(next.bearing);
    if (curr.zoom != next.zoom) thismap.setZoom(next.zoom);
});

function getLocation() {
    console.log("is this ran?")
    //console.log(props.modelValue)
    // console.log(thismap.value.getZoom())
    return {
        ...thismap.value.getCenter(),
        bearing: thismap.value.getBearing(),
        pitch: thismap.value.getPitch(),
        zoom: thismap.value.getZoom()
    }
}

</script>

<template>
    <div ref="mapContainer" class="map-container"></div>
</template>  

<style>
.map-container {
    flex: 1;
}
</style>

It looks like the object does not get updated after the emit, cause props.modelValue does not change values. Also, if there is anything else I am doing wrong here, I would love to receive your feedback.


Solution

  • v-model is syntax sugar for:

    <Map :modelValue="location" @update:modelValue="location = $event" />
    

    The only way for the reactivity to work this way is to make location a ref. ref should be chosen over reactive when an object is expected to be fully replaced at some point:

    const location = ref({...});
    

    This works with event listener due to ref unwrapping in templates, location = $event actually means location.value = $event.