Add dynamic SVG to Vue template without any wrapper

Viewed 109

i want to add dynamic SVG code to my <template> but without passing it to v-html or without any wrapper around it.

Because the end result should be something like this, and as far as u know template doesn't support v-html. But still if there's way to achieve this Result with v-html or any work around then it would be perfect.

<template>
  <svg>
     
  </svg>
<template>

My code looks like this.

<template>
  <div>
    <span v-html="svgData"></span>
  </div>
</template>

<script setup lang="ts">
  import { computed } from "vue";
  import type { mainIcon } from "tester";
  import { completeDataSet } from "tester";

  const props = defineProps<{
    icon: mainIcon;
  }>();

  const iconPassed = completeDataSet.find((item) => item.name === props.icon);
  const svgData = computed(() => iconPassed?.data);
</script>

Note: I am getting SVG as a String data returned by 3rd party Library. Which is why i cannot make changes to SVG Structure.

Thanks in Advance.

2 Answers

The answer depends on how svgs are stored and received. Some libraries (@mdi/svg) provide not the file itself, but only the path value. Then it can be used without a wrapper in an obvious way

<template>
  <svg viewBox="..">
    <path :d="path" />
  </svg>
</template>

Another case is about physical svg files. If that's just several files included into src/assets, they can be loaded and used as components. Here's the example for webpack:

<template>
  <component :is="component" />
</template>

<script>
const iconComponents = {}
const req = require.context('@/assets/', true, /\.svg$/)
req.keys().forEach((filename) => {
  const nameParts = filename.split('/')
  const name = nameParts[nameParts.length - 1].replace(/\.svg$/, '')
  iconComponents[name] = req(filename)
})

export default defineComponent({
  props: {
    iconName: {
      type: String,
      required: true
    }
  }
  computed: {
    component() {
      return iconComponents[this.iconName]
    }
  }
})
</script>   

Instead of v-html, the icon component could manually replace the contents of an <svg> with the SVG markup from an imported file:

  1. Apply a template ref to an <svg> in your component template:
<template>
  <div>
    <svg ref="svgRef"></svg>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const svgRef = ref(null)
</script>
  1. Add a watcher that updates the SVG ref's inner HTML with the SVG that corresponds to the icon property, and await a nextTick to allow the HTML to take effect in the DOM:
<script setup>
import { computed, nextTick, watchEffect, ref } from 'vue'
import * as icons from 'simple-icons/icons'

const props = defineProps({ icon: String })
const iconSet = Object.entries(icons)
const svgData = computed(() => iconSet.find(([iconName, icon]) => iconName === props.icon)?.[1].svg)
const svgRef = ref(null)

watchEffect(async () => {
  if (svgRef.value) {
    svgRef.value.innerHTML = svgData.value
    await nextTick()

    ⋮
})
</script>
  1. We need to keep the existing SVG ref's element in the virtual DOM so that Vue can track it properly. If the first child of the SVG ref is another <svg>, replace the contents of that <svg> with its own children since the SVG ref itself is already an <svg>. Make sure to copy the inner <svg>'s attributes to the SVG ref beforehand:
<script setup>
⋮

const removeAttributes = (el) => {
  ;[...el.attributes].forEach((attr) => el.removeAttribute(attr.name))
}
const copyAttributes = (fromEl, toEl) => {
  ;[...fromEl.attributes].forEach((attr) => toEl.setAttribute(attr.name, attr.value))
}

watchEffect(async () => {
  if (svgRef.value) {
    ⋮

    const firstChild = svgRef.value.firstChild
    if (firstChild.tagName.toLowerCase() === 'svg') {
      removeAttributes(svgRef.value)
      copyAttributes(firstChild, svgRef.value)

      // replace inner SVG with its children, as the container is already an SVG
      firstChild.replaceWith(...firstChild.children)
    }
  }
})
</script>

demo

Related