Sachin Kumar
Sachin Kumar

Reputation: 3240

Rendering Vue 3 component into HTML string

I am working on a vue.js project(version 3). I come to a situation where I want to use the rendered HTML view of a component to my current component's method.

I created the following SVG component in my Vue project.

CircleWithTextSvg.vue

<template>
    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" width="20" height="20">
        <g id="UrTavla">
            <circle cx="10" cy="10" r="10" stroke="gray" stroke-width="1" :fill="fill" />
            <text x="50%" y="50%" text-anchor="middle" stroke="black" stroke-width="1px" dy=".3em">{{text}}</text>
        </g>
    </svg>
</template>

<script>
    export default {
        props: {
            text: { 
                type: String, 
                default: "1"
            },
            fill: { 
                type: String, 
                default: "white"
            }
        }
    }
</script>

This component basically renders a circle with text inside. If I use this component in my main component's template section like the following then it works fine(obviously)

MainComponent.vue

<template>
    <circle-with-text-svg />
</template>

But I want to send this SVG component rendered output as an option to the third party.

Real Use Case:- Actually, I created this separate component to show as a marker on my leaflet map. The problem is now I want to use this SVG component inside my MainComponent's method so I can use it as an option to L.divIcon

When I try the following

export default {
    methods: {
        generateMap(element) {
            // ...
            let icon = L.divIcon({ 
                html: <circle-with-text-svg 
                    fill="'#D3D5FF'"
                    text="1" />, 
                iconSize: [10, 10]
            });
            // ...
        }
    }
}

Then it gives the errors

Support for the experimental syntax 'JSX isn't currently enabled

In the react, we can simply use the component inside the template of another component normally. but how can we achieve this in vue.js

By looking at the error, it seems like JSX experimental is not enabled.

Can someone address me how can I achieve this?

Upvotes: 5

Views: 13074

Answers (2)

Michal Lev&#253;
Michal Lev&#253;

Reputation: 37883

Ok, so in comments I recommended the answer for question How to get the compiled html content of a component in vuejs which is actually implemented for Vue 2

I was curious if this works in Vue 3 and you can see the result below. Here are changes required:

  1. Obvious changes is replacing new Vue with createApp and using global h() instead the one passed into render()
  2. In Vue 2, you can call $mount() function of main Vue instance without the argument. That worked because Vue created DOM element to mount to in memory. This is not the case in Vue 3 - you need to provide the element yourself
  3. One big change not used in my example but very important for some use cases is that in Vue 3 components registered as global in main app instance using app.component() are not accessible in the tempApp used to render HTML. All components in use must be registered in appropriate instance - see the migration guide

// We use component options object with string template
// In the proper Vue app (with Webpack and Vue SFC) this whole block can be replaced with "import CircleWithTextSvg from CircleWithTextSvg.vue"
const CircleWithTextSvg = {
  name: 'CircleWithTextSvg',
  props: {
    text: {
      type: String,
      default: "1"
    },
    fill: {
      type: String,
      default: "white"
    }
  },
  template: `
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" width="20" height="20">
        <g id="UrTavla">
            <circle cx="10" cy="10" r="10" stroke="gray" stroke-width="1" :fill="fill" />
            <text x="50%" y="50%" text-anchor="middle" stroke="black" stroke-width="1px" dy=".3em">{{text}}</text>
        </g>
    </svg>
  `
}

const app = Vue.createApp({
  mounted() {
    console.log(this.getIconHtml('Hello!'))
  },
  methods: {
    getIconHtml(text, type) {
      const tempApp = Vue.createApp({
        render() {
          return Vue.h(CircleWithTextSvg, {
            text,
            type
          })
        }
      })

      // in Vue 3 we need real element to mount to unlike in Vue 2 where mount() could be called without argument...
      const el = document.createElement('div');
      const mountedApp = tempApp.mount(el)

      return mountedApp.$el.outerHTML
    }
  }
})

app.mount('#app')
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<div id='app'>
</div>

Note 1: Code above is intended to run directly in the browser where import is not available. For that reason we use Vue global build and accessing Vue global API's using for example Vue.createApp or Vue.h. In regular Vue app you need to import those functions as import { createApp, h } from 'Vue'

Note 2: Arguably if the HTML fragmets used with Leaflet components are so simple as your CircleWithTextSvg component, much more simple and performant would be to define them not as Vue components but as template literals

Upvotes: 8

Fanna1119
Fanna1119

Reputation: 2316

I tried the above solution but it did not budge for me, the $el.outerHTML did not feel right and kept throwing null and undefined because of the life cycle in the rest of my app. In vue3 I did this. it still does not feel right. But getting there :)


const getIconHtml = (item, element) => render(h(CircleWithTextSvg, {
  onSomeEmit: (ev) => {
    
  },
  text: item, //props
}), element //element to render to
)

In my problem I had to return a template for a 3rd party js(visjs) library

so how i used it was something like this.

import {render, h} from 'vue';

const itemTemplate = async (item, element) => render(h(bar, {
  onDelete: (ev) => {
    vis.removeItem(item.id)
  },
  item: item,
}), element
)

//note this template key does not refer to vuejs template
  template: (item, element) => {
    if (!item) return;
    return itemTemplate(item, element);
  }


Things to consider using this approach. This technically creates a new vue instance, so any additional apps, props, components, directives you want to use, you might need to manually register to this component. some globals might work, but I had issues getting directives to work because of some deep issue,

Upvotes: 0

Related Questions