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