javascriptvue.jsvuejs3yandex-maps

Vue 3: Why vue yandex maps package doesn't work in vue 3 with defineCustomElement feature?


I have 2 projects with vue yandex maps in vue 3:

First project

Demo first project where work vue yandex maps. In this project package registered like this:

Code main.js where registered vue-yandex-maps components from js file:

const { createApp } = require('vue');
import App from './App.vue';
import ymapPlugin from 'vue-yandex-maps/dist/vue-yandex-maps.esm.js';

const app = createApp(App);

app.config.isCustomElement = (tag) => tag.startsWith('y'); // <= This is doesn't work
app.use(ymapPlugin);
app.mount('#app');

Code MapComponent.vuewhere used package vue-yandex-maps:

<template>
  <yandex-map :coords="coords">
    <ymap-marker
      marker-id="123"
      :coords="coords"
      :marker-events="['click']"
    ></ymap-marker>
  </yandex-map>
</template>

<script>
export default {
  name: 'MapComponent',
  setup() {
    return {
      coords: [54, 39],
    };
  },
};
</script>

Code App.vuewhere used component MapComponent:

<template>
  <div id="app">
    <MapComponent />
  </div>
</template>

<script>
import MapComponent from './components/MapComponent.vue';

export default {
  name: 'App',
  components: {
    MapComponent,
  },
};
</script>

Second project

Demo second project where used new feature defineCustomElement from vue version 3.2 and get error message when use package vue-yandex-maps:

Uncaught TypeError: Cannot read properties of null (reading 'offsetWidth')

Code main.js where registered vue-yandex-maps components from js file:

import { defineCustomElement } from './defineCustomElementWithStyles'
import App from './App.ce.vue'
import store from './store'
import router from './router'
import ymapPlugin from 'vue-yandex-maps/dist/vue-yandex-maps.esm.js'

customElements.define(
  'app-root',
  defineCustomElement(App, {
    plugins: [store, router, ymapPlugin],
  })
)

Code defineCustomElementWithStyles.js:

import { defineCustomElement as VueDefineCustomElement, h, createApp, getCurrentInstance } from 'vue'

const getNearestElementParent = (el) => {
  while (el?.nodeType !== 1 /* ELEMENT */) {
    el = el.parentElement
  }
  return el
}

export const defineCustomElement = (component, { plugins = [] }) =>
  VueDefineCustomElement({
    props: component.props,
    setup(props) {
      const app = createApp()

      // install plugins
      plugins.forEach(app.use)

      app.mixin({
        mounted() {
          const insertStyles = (styles) => {
            if (styles?.length) {
              this.__style = document.createElement('style')
              this.__style.innerText = styles.join().replace(/\n/g, '')
              getNearestElementParent(this.$el).prepend(this.__style)
            }
          }

          // load own styles
          insertStyles(this.$?.type.styles)

          // load styles of child components
          if (this.$options.components) {
            for (const comp of Object.values(this.$options.components)) {
              insertStyles(comp.styles)
            }
          }
        },
        unmounted() {
          this.__style?.remove()
        },
      })

      const inst = getCurrentInstance()
      Object.assign(inst.appContext, app._context)
      Object.assign(inst.provides, app._context.provides)
      console.log({ props })
      return () => h(component, props)
    },
  })

Code Home.ce.vue where used component MapComponent:

<script>
export default {
  name: 'Home',
}
</script>

<script setup>
import HelloWorld from '@/components/HelloWorld.ce.vue'
import MapComponent from '@/components/MapComponent.ce.vue'
</script>

<template>
  <h2>Home</h2>
  <HelloWorld msg="hello world" />
  <MapComponent />
</template>

Code MapComponent.ce.vue where used package vue-yandex-maps:

<template>
  <yandex-map :coords="coords">
    <ymap-marker marker-id="123" :coords="coords" :marker-events="['click']"></ymap-marker>
  </yandex-map>
</template>

<script>
export default {
  name: 'MapComponent',
  setup() {
    return {
      coords: [54, 39],
    }
  },
}
</script>

<style>
.ymap-container {
  height: 600px;
}
</style>

Question

Where I have error in second project where I use vue-yandex-maps with defineCustomElement?


Solution

  • vue-yandex-maps renders a map container with a randomly generated ID that is passed to the ymaps.Map constructor, which later uses it to query the document for the element. Unfortunately, the map container is rendered inside the Shadow DOM of the app-root custom element, which is hidden from document queries. The document.querySelector() thus returns null, and the ymaps.Map code tries to get the size of the container via the null reference, leading to the error you observed.

    You would have to patch vue-yandex-maps yourself, or submit a GitHub issue to request a feature change, where you could pass in the map container element (from the custom element's Shadow DOM) instead of an ID. It looks like ymaps.Map already accepts either an element or a string ID, so no other change would be necessary.