Audiosleef
Audiosleef

Reputation: 137

Container element not found when rendering from slot in parent

I am passing a slot from a parent to multiple children. In one of the child components, the div-id is not found, possibly because the slot is not being rendered fast enough and the id is not found when the component is mounted.

When spinning the application up, this results in

Map container element not found

Thrown because the map-google id was not found.

The child component:

<template>
  <MapTileComponent
    title="Google Maps"
    @update:address="onAddressChanged"
    @update:route="onRouteChanged"
    @update:coordinates="onCoordinatesChanged"
  >
    <template #info>
      <title>Add Map</title>
    </template>
    <template #map2>
      <div id="map-google"></div>
    </template>
  </MapTileComponent>
</template>

<script setup lang="ts">
import axios from "axios";
import { Loader } from "google-maps";

const apiKey = "myKey";

const loader = new Loader(apiKey, {
  version: "weekly",
  libraries: ["marker"], // Load the marker library
});

const googleMap = ref<google.maps.Map | null>(null);

// Variables for map and marker
let map: google.maps.Map;
let marker: google.maps.Marker;

onMounted(() => {
  // Ensure the DOM is fully updated, including slots, before accessing the element
  nextTick().then(() => {
    const mapElement = document.getElementById("map-google");
    if (mapElement) {
      loader
        .load()
        .then((google) => {
          const center = new google.maps.LatLng(51.147578, 4.436328);

          // Initialize the map
          map = new google.maps.Map(mapElement, {
            center: center,
            zoom: 15,
          });

          // Assign map to the reactive variable
          googleMap.value = map;

          // Initialize the marker
          marker = new google.maps.Marker({
            position: center,
            map: map,
            title: "Your Location",
          });
        })
        .catch((error) => {
          console.error("Error loading Google Maps API:", error);
        });
    } else {
      console.error("Map container element not found.");
    }
  });
});

const onAddressChanged = (address: string) => {
  fetchCoordsByAddress(address);
};

const fetchCoordsByAddress = async (address: string) => {
  try {
    axios
      .get(
        `https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=${apiKey}`
      )
      .then((response) => {
        if (response.data.status === "OK") {
          let lat = response.data.results[0].geometry.location.lat;
          let lng = response.data.results[0].geometry.location.lng;
          const location = new google.maps.LatLng(lat, lng);
          console.log(location);
          // Update map center and marker position
          map.setCenter(location);
          marker.setPosition(location);
        }
      });
  } catch (error) {
    console.error(error);
  }
};

const onCoordinatesChanged = (coordinates: Number[]) => {
  fetCoordsByCoordindates(coordinates);
};

const fetCoordsByCoordindates = async (coordinates: Number[]) => {
  try {
    axios
      .get(
        `https://maps.googleapis.com/maps/api/geocode/json?latlng=${
          (coordinates[0], coordinates[1])
        }&key=${apiKey}`
      )
      .then((response) => {
        if (response.data.status === "OK") {
          let lat = response.data.results[0].geometry.location.lat;
          let lng = response.data.results[0].geometry.location.lng;
          const location = new google.maps.LatLng(lat, lng);

          map.setCenter(location);
          marker.setPosition(location);
        }
      });
  } catch (error) {
    console.error(error);
  }
};

const onRouteChanged = () => {};
</script>

<style>
#map {
  width: 100%;
  height: 100%;
  min-height: 380px;
  padding: 0;
  margin: 0;
}
</style>

MapGrid component:

<template>
    <v-row>
      <v-col>
        <slot name="map1"></slot>
      </v-col>
      <v-col>
        <slot name="map2"></slot>
      </v-col>
    </v-row>
    <v-row>
      <v-col>
        <slot name="map3"></slot>
      </v-col>
      <v-col>
        <slot name="map4"></slot>
      </v-col>
    </v-row>
</template>

Consisting of the MapTileComponents:

<template>
  <v-container>
    <!-- Menu container with transitions -->
    <div class="ma-4 menu-container">
      <v-btn
        style="position: relative; z-index: 2"
        @click="toggleMenu()"
        id="menuButton"
        >{{ title }}</v-btn
      >
      <v-slide-x-transition hide-on-leave>
        <div v-if="showMenu" key="main-menu" class="menu">
          <v-list>
            <v-list-item
              v-for="(menuItem, index) in subMenus"
              :key="index"
              @click="toggleSubMenu(menuItem)"
              append-icon="mdi-chevron-right"
            >
              <v-list-item-title>{{
                menuItem.title
              }}</v-list-item-title></v-list-item
            >
          </v-list>
        </div>
      </v-slide-x-transition>

      <v-slide-x-reverse-transition hide-on-leave>
        <div v-if="showSubmenu" key="submenu" class="menu">
          <v-list>
            <v-list-item
              @click="toggleSubMenu(undefined)"
              append-icon="mdi-chevron-left"
            >
              <v-list-item-title>{{ currentSubMenu?.title }}</v-list-item-title>
            </v-list-item>
            <v-list-item
              v-for="(item, index) in currentSubMenu?.items"
              :key="index"
            >
              <v-list-item-subtitle v-if="item.subtitle">{{
                item.subtitle
              }}</v-list-item-subtitle>
              <component :is="item.component()"></component>
            </v-list-item>
          </v-list>
        </div>
      </v-slide-x-reverse-transition>
    </div>

    <slot name="map"></slot>
  </v-container>
</template>

<script setup lang="ts">
import { debounce } from "lodash";
import { VNode } from "vue";
import { VBtn, VSwitch, VTextField } from "vuetify/components";

interface ISubMenu {
  title: string;
  items: Array<{
    subtitle?: string;
    component: () => VNode;
  }>;
}

const props = defineProps({
  title: String,
});

const emit = defineEmits<{
  (e: "update:address", value: string): void;
  (e: "update:coordinates", value: Number[]): void;
  (e: "update:route", value: { start: string; end: string }): void;
  (e: "update:environment", value: { key: string; flag: boolean }): void;
}>();

const showMenu = ref(false);
const showSubmenu = ref(false);
const currentSubMenu = ref<ISubMenu>();
const routeStart = ref("Something");
const routeEnd = ref("Something");

const subMenus = ref([
  {
    title: "Geocode",
    items: [
      {
        subtitle: "Forward",
        component: () =>
          h(VTextField, {
            placeholder: "Enter an address",
            variant: "outlined",
            "onUpdate:modelValue": (val: string) => {
              debounceAddress(val);
            },
          }),
      },
      {
        subtitle: "Reverse",
        component: () =>
          h(VTextField, {
            placeholder: "Enter coordinates",
            variant: "outlined",
            "onUpdate:modelValue": (val: string) => {
              debounceCoordinates(val);
            },
          }),
      },
    ],
  },
  {
    title: "Routing",
    items: [
      {
        component: () =>
          h(VTextField, {
            placeholder: "Enter a start",
            modelValue: routeStart,
            variant: "outlined",
          }),
      },
      {
        component: () =>
          h(VTextField, {
            placeholder: "Enter a destination",
            modelValue: routeEnd,
            variant: "outlined",
          }),
      },
      {
        component: () =>
          h(VBtn, {
            text: "Search",
            style: "margin-bottom: 10px",
            onClick: () => {
              toggleMenu()
              emit("update:route", {
                start: routeStart.value,
                end: routeEnd.value,
              });
            },
          }),
      },
    ],
  },
  {
    title: "Environment",
    items: [
      {
        component: () =>
          h(VSwitch, {
            label: "Air quality",
            key: "AirQuality",
            "onUpdate:modelValue": (val: any) =>
              emit("update:environment", { key: "AirQuality", flag: val }),
          }),
      },
    ],
  },
]);

// debounce
const debounceAddress = debounce((value: string) => {
  emit("update:address", value);
}, 1000);

const debounceCoordinates = debounce((value: string) => {
  emit("update:coordinates", getCoordinates(value));
}, 1000);

// misc
const getCoordinates = (coordVal: string): Number[] => {
  try {
    const [x, y] = coordVal.split(",");
    return [parseFloat(x.trim()), parseFloat(y.trim())];
  } catch (error) {
    throw error;
  }
};

// menu
const toggleMenu = () => {
  if (showSubmenu.value) {
    showMenu.value = false;
    showSubmenu.value = false;
  } else {
    showMenu.value = !showMenu.value;
  }
};

const toggleSubMenu = (subMenu: ISubMenu | undefined) => {
  showMenu.value = !showMenu.value;
  showSubmenu.value = !showSubmenu.value;
  currentSubMenu.value = subMenu;
};
</script>

<style scoped>
.v-container {
  height: 100%;
  margin: 0;
  padding: 0;
}
.v-card {
  height: 100%;
  min-height: 100%;
}

.menu-container {
  position: absolute;
  max-width: 25%;
  z-index: 2;
}

.menu {
  position: relative;
  width: 175%;
  z-index: 2;
  top: 4px;
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
}

.menu .v-list {
  padding-top: 0px;
  border-radius: 6px;
}
</style>

Upvotes: 0

Views: 34

Answers (0)

Related Questions