6

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?

6
  • Does this answer your question? How to get the compiled html content of a component in vuejs Commented Jul 9, 2021 at 11:49
  • Don't you think this will make more overheads? As I have iterate over too long lists and each list element will create its own vue app instance for SVG. Commented Jul 9, 2021 at 12:03
  • BTW, why it is not working with vue 3. Is this for vue 2? Commented Jul 9, 2021 at 12:03
  • Yes, linked answer is for Vue 2 and needs to be changed slightly to work with Vue 3 (createApp instead of new Vue mainly) but the principle is the same - create new app, mount it, retrieve rendered HTML. Regarding the overhead - createApp is very similar as rendering component in template - both app and component are instances of Vue. Commented Jul 9, 2021 at 12:10
  • It will better if you post your answer for vue 3. I can't convert the vue 2 app to vue 3. Thanks in advance Commented Jul 9, 2021 at 12:13

2 Answers 2

9

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

Sign up to request clarification or add additional context in comments.

Comments

0

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,

1 Comment

Interesting and simple solution. I'm just not sure about the render API - wasn't able to find any info in Vue documentation. More often than not this means the API is not intended to be public. Do you have any links to docs ?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.