jpkleemans/vite-svg-loader

How can we dynamically import svg files?

Closed this issue ยท 17 comments

I am not sure if this loader supports this:

  computed: {
    currentIcon() {
        return () => import('path/to/svg/file.svg');
    }
  }

and then we can use as

<template>
  <compoment
    :is="currentIcon"
    :aria-hidden="hidden"
  />
</template>

Currently console output as

runtime-core.esm-bundler.js:38 [Vue warn]: Invalid VNode type: undefined (undefined) 
  at <Anonymous is=fn aria-hidden=true > 
  at <UiIcon name="notification" > 

Hello! I was trying to do something similar and after several ideas that occurred to me, I managed to do it this way:

  1. Put in assets/icons all the svg that you will use. Create a file index.js and import and export the svg
// src/assets/icons/index.js
import add from './add.svg'
import multiply from './multiply.svg'
// etc

export {
  add,
  multiply,
  // etc
}
  1. In path src/components, create component Icon.vue:
// src/components/Icon.vue
<template>
  <component :is="icon"/>
</template>

<script>
import * as icons from "../assets/icons";

export default {
  name: "Icon",
  props: ["name"],
  computed: {
    icon() {
      return icons[this.name]
    }
  },
};
</script>
  1. Use it!
<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3 + Vite" />
  <icon name="add" />
  <icon name="multiply" />
</template>

<script setup>
import HelloWorld from './components/HelloWorld.vue'
import Icon from "./components/Icon.vue";
</script>

IMPORTANT NOTE: If your svg has a name in kebab-case (i. e "heart-pulse.svg"), when you use the icon component, the prop name must be in snake_case (i. e "heart_pulse")

Haha, i end up almost the same way... the down side of this is that we have to include all svg files even we don't use them on the current page. well, for now this is the way i guess. Cheers!

@MuzafferDede you could use something like this to support lazy loading:

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

<script>
export default {
  data () {
    return {
      currentIcon: null
    }
  },
  async created () {
    this.currentIcon = await import('./assets/Bananas.svg')
  }
}
</script>

@MuzafferDede you could use something like this to support lazy loading:

This still does not work. Have you tried?

Guys, you may try this plugin:

https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars

It works well on localdev, but I haven't figured out how to use it on production.

@MuzafferDede you could use something like this to support lazy loading:

This still does not work. Have you tried?

Yes, it works for me (using vite v2.4.4)

@jpkleemans
i get Cannot convert object to primitive value when i use your suggestion.

I come out with this which works

import { defineAsyncComponent } from 'vue'
export default {
  props: {
    name: {
      type: String,
      default: undefined,
    },
  },
  computed: {
    currentIcon() {
      return defineAsyncComponent(() => import(`../../assets/icons/svg/${this.name}.svg`))
    },
  },
};

@MuzafferDede ah, that's an even better solution!

Have some solution for vue-2?

You can do it with Vue 2 thanks to the @vue/composition-api package ๐Ÿ‘

import { defineAsyncComponent } from '@vue/composition-api';

For those wanting to use <script setup> you can do this. This worked for me on Nuxt3.

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

<script lang="ts" setup>
const props = defineProps({
  name: {
    type: String,
    required: true
  },
});

const icon = defineAsyncComponent(() => import(`../../assets/icons/${props.name}.svg`));
</script>

how could you make it work to have a :src instead of an :is ?

9mm commented

Just to be clear, if I use defineAsyncComponent, is that going to try to load everything via URL then? Or will it figure it out at build time?

I wanted to retain the component style where the SVG's are actually inlined

I think I found the best beautiful solution :)
Vite docs: https://vitejs.dev/guide/features.html#glob-import

Template:

<component :is="icon" :class="className" />

Script:

import { defineAsyncComponent } from 'vue'

export default {
  props: {
    name: {
      type: String,
      required: true
    }
  },

  data() {
    return {
      icons: import.meta.glob(`./**/*.svg`)
    }
  },

  computed: {
    icon() {
      return defineAsyncComponent(() => this.icons[`./${this.name}.svg`]())
    },
  }
}

This is not a real dynamic import : import.meta.glob('./**/*.svg') clearly say "I will import ALL icons according the glob pattern", and that is not what we want in real dynamic import.

This is not a real dynamic import : import.meta.glob('./**/*.svg') clearly say "I will import ALL icons according the glob pattern", and that is not what we want in real dynamic import.

Please write documentation from my link :)

Still no real dynamic import? by the real url