Skip to content

Embed with Vue 3 and Nuxt ​

Use the Mapsted Maps JavaScript API inside a Vue 3 or Nuxt 3 application. This tutorial covers the <script setup> composition API pattern, onMounted / onUnmounted lifecycle hooks, Nuxt SSR safety via <ClientOnly>, and the .client.vue naming convention.

Prerequisites ​

  • Node.js 20 or later
  • npm 9 or later
  • Vue 3.4+ (plain Vite project) or Nuxt 3+
  • Basic familiarity with the Vue 3 Composition API

1. Install the API ​

bash
npm install @mapsted/maps-js-api

Licensed customers

The @mapsted/maps-js-api package is currently access-restricted on npm. If npm install returns 404 or 401, contact your Mapsted account manager for an npm token, then run npm config set //registry.npmjs.org/:_authToken <token> before re-running install.

2. Vue 3 component pattern (Vite + Vue 3) ​

Create a MapstedMap.vue component using <script setup>. Use a ref for the template ref (DOM container) and onMounted / onUnmounted for the map lifecycle.

vue
<!-- src/components/MapstedMap.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { init } from '@mapsted/maps-js-api';
import type { MapInstance } from '@mapsted/maps-js-api';

const props = defineProps<{
  propertyId: number;
}>();

const containerRef = ref<HTMLDivElement | null>(null);
let mapInstance: MapInstance | null = null;

onMounted(async () => {
  if (!containerRef.value) return;

  mapInstance = await init({
    element: containerRef.value,
    propertyId: props.propertyId,
    onload: () => {
      console.log('Mapsted map ready');
    },
  });
});

onUnmounted(async () => {
  await mapInstance?.destroy();
  mapInstance = null;
});
</script>

<template>
  <div ref="containerRef" style="width: 100%; height: 100vh;" />
</template>

Use it in your app:

vue
<!-- src/App.vue -->
<script setup lang="ts">
import MapstedMap from './components/MapstedMap.vue';
</script>

<template>
  <MapstedMap :property-id="1643" />
</template>

3. Nuxt 3 ​

The Mapsted API is browser-only. It accesses window, document, and creates an <iframe>. In Nuxt 3, components run on the server during SSR unless you explicitly prevent it.

Option A — <ClientOnly> wrapper ​

Wrap the component with Nuxt's built-in <ClientOnly> component in any page or layout. The map component will be skipped on the server and hydrated in the browser:

vue
<!-- pages/index.vue -->
<script setup lang="ts">
import MapstedMap from '~/components/MapstedMap.vue';
</script>

<template>
  <main>
    <ClientOnly>
      <MapstedMap :property-id="1643" />
      <template #fallback>
        <div style="width: 100%; height: 100vh; background: #f0f0f0;">
          Loading map…
        </div>
      </template>
    </ClientOnly>
  </main>
</template>

The #fallback slot renders on the server and during hydration, preventing a layout shift while the map loads.

Option B — .client.vue suffix ​

Nuxt auto-detects the .client.vue suffix and skips server rendering for that component entirely. Rename the file to MapstedMap.client.vue:

components/
  MapstedMap.client.vue   ← Nuxt skips SSR for this file automatically

No <ClientOnly> wrapper is needed — Nuxt handles it. The component source is identical to the plain Vue 3 version above.

Which option should I use?

Both options produce the same SSR behaviour. .client.vue is convenient for components that are always browser-only. <ClientOnly> is better when you need the fallback slot for server-rendered placeholder content.

4. Pinia store integration ​

If multiple Vue components need access to the same map instance — for example, a floor selector panel and a search bar both issuing API calls — store the MapInstance handle in a Pinia store.

ts
// stores/map.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { init } from '@mapsted/maps-js-api';
import type { MapInstance } from '@mapsted/maps-js-api';

export const useMapStore = defineStore('map', () => {
  const instance = ref<MapInstance | null>(null);
  const ready = ref(false);

  async function mount(element: HTMLElement, propertyId: number) {
    instance.value = await init({
      element,
      propertyId,
      onload: () => { ready.value = true; },
    });
  }

  async function unmount() {
    await instance.value?.destroy();
    instance.value = null;
    ready.value = false;
  }

  return { instance, ready, mount, unmount };
});

Then call store.mount(el, propertyId) from onMounted and store.unmount() from onUnmounted. Other components import useMapStore() and read store.instance to call methods like navigateToFloorById or selectEntity.

5. Live sandbox preview ​

The sandbox below runs the same map using the CDN-global API, showing what your component will render:

6. Custom composable recipe ​

For a reusable useMapstedMap() composable that wraps the pattern above, see the companion guide: useMapstedMap() composable recipe

Next steps ​