Vue-svg-loader: Dynamically refer to an icon

Created on 8 Oct 2018  ·  15Comments  ·  Source: visualfanatic/vue-svg-loader

When I was using svgs as images and using require, I was able to dynamically require images. So I could have a folder of 1000 images and I wouldn't have to require them each by hand. In the component template I had <img :src=iconPath(dynamicValue)>.

iconPath is a method on a vue component like this

iconPath(dynamicValue) {
  return require(`./icons/${dynamicValue}.svg`)
}

Every svg file in ./icons would get included in my webpack bundle automatically, and then based on the value of dynamicValue, the correct image would appear in the component template.

I can't find a way to do it with vue-svg-loader without having to specify all the files themselves in some kind of icons/index.js file that has all the icons exported, so I can do import { MyIcon } from './icons'. Would there be a way to make it work like that?

Docs mention

Most helpful comment

I've tested this with an older version! I'm sorry.

Since 0.11.0 (https://github.com/visualfanatic/vue-svg-loader/commit/1fa688da88e0806f335254af1640c6ebdd2a845a, https://github.com/visualfanatic/svg-to-vue/commit/151c5d7380f2c1246ec2efec007d626f60597b6d), this loader exports the generated Vue components as ES modules, so you need to change your require too, adding .default to the end:

Well, you were quicker than me!


Althought, thinking about it, you should use dynamic imports to enjoy the *magic* of code splitting:

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

<script>
export default {
  name: 'DynamicIcon',
  props: {
    icon: {
      type: String,
      required: true,
    },
  },
  computed: {
    dynamicIcon () {
      return () => import(
        /* webpackChunkName: "icons" */
        /* webpackMode: "lazy-once" */ 
        `./icons/${this.icon}.svg`
      )
    },
  },
}
</script>

Take notice that this only works if you do the import on a computed property. For some reason that I don't know, it fails on a component method. The webpackChunkName and webpackMode comments are there to ensure Webpack includes all the SVGs of that path inside a single chunk named icons. Refer to this article for more information about the "magic comments".

Then you can reuse the component above throughout your project, allowing you to add multiple dynamic icons to the same component (and not rely on multiple computed properties):

<template>
  <div>
    <DynamicIcon icon="icon-one"/>
    <DynamicIcon icon="icon-two"/>
    <DynamicIcon icon="icon-three"/>
  </div>
</template>

<script>
import DynamicIcon from './DynamicIcon.vue'

export default {
  name: 'IconList',
  components: {
    DynamicIcon,
  },
}
</script>

All 15 comments

You can use dynamic components.

Simply change <img :src=iconPath(dynamicValue)> to <component :is='iconPath(dynamicValue)'/>.

If I print the value by putting {{ iconPath(dynamicValue) }} in the template it shows { "default": { "functional": true } } but in the console I get [Vue warn]: Failed to mount component: template or render function not defined. with <component :is='iconPath(dynamicValue)'/>.

It works fine when iconPath remains the same but I use file-loader and the img :src tag instead of vue-svg-loader and component :is. Anything wrong you see?

Hi, thanks for help guys! I managed to fix it by added default to the end of method:
iconPath(name) {
return require(@/assets/icons/icon_${name}.svg).default;
}

I've tested this with an older version! I'm sorry.

Since 0.11.0 (https://github.com/visualfanatic/vue-svg-loader/commit/1fa688da88e0806f335254af1640c6ebdd2a845a, https://github.com/visualfanatic/svg-to-vue/commit/151c5d7380f2c1246ec2efec007d626f60597b6d), this loader exports the generated Vue components as ES modules, so you need to change your require too, adding .default to the end:

Well, you were quicker than me!


Althought, thinking about it, you should use dynamic imports to enjoy the *magic* of code splitting:

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

<script>
export default {
  name: 'DynamicIcon',
  props: {
    icon: {
      type: String,
      required: true,
    },
  },
  computed: {
    dynamicIcon () {
      return () => import(
        /* webpackChunkName: "icons" */
        /* webpackMode: "lazy-once" */ 
        `./icons/${this.icon}.svg`
      )
    },
  },
}
</script>

Take notice that this only works if you do the import on a computed property. For some reason that I don't know, it fails on a component method. The webpackChunkName and webpackMode comments are there to ensure Webpack includes all the SVGs of that path inside a single chunk named icons. Refer to this article for more information about the "magic comments".

Then you can reuse the component above throughout your project, allowing you to add multiple dynamic icons to the same component (and not rely on multiple computed properties):

<template>
  <div>
    <DynamicIcon icon="icon-one"/>
    <DynamicIcon icon="icon-two"/>
    <DynamicIcon icon="icon-three"/>
  </div>
</template>

<script>
import DynamicIcon from './DynamicIcon.vue'

export default {
  name: 'IconList',
  components: {
    DynamicIcon,
  },
}
</script>

Thanks for this explanation!

Is there a way to customize where the .js files that get produced are placed? All the .js files that get made for each svg are placed in the root of the webpack output directory, which makes that root output directory cluttered. Can it be configured to put them in another directory, like to take an options item like other loaders do, something like

options: {
    name: 'icons/icon-[name]-[hash].[ext]',
},

So then all icons that vue-svg-loader operates on will be placed in that one directory. Could this feature be added?

@rightaway I am currently working on a Babel plugin that would allow you to transform one import statement, for example:

import icons from '@/assets/icons';

console.log(icons); // { SomeIcon: 'component declaration', AnotherIcon: '...' }

... into multiple statements:

import SomeIcon from '@/assets/icons/some-icon.svg';
import AnotherIcon from '@/assets/icons/another_icon.svg';

const icons = { SomeIcon, AnotherIcon };

console.log(icons);

It would scan the @/assets/icons directory for the icons, transform their names (some_icon ➡️ SomeIcon), transform that single import into multiple ones and provide them in a single object.

That plugin would also allow simplify multiple imports:

import MyIcon from '@/assets/icons/my-icon.svg';
import SomeOtherIcon from '@/assets/icons/some_other-icon.svg';
import AnotherIcon from '@/assets/icons/anotherIcon.svg';

... into a single one:

import { MyIcon, SomeOtherIcon, AnotherIcon } from '@/assets/icons';

It has some other features like dynamic imports, custom icon and import name formatter, and ability to group webpack chunks into groups, so your app would make less requests to async SVG components.

Would that plugin help you in any way? I am finishing the code and tests, so it should be available really soon.

@visualfanatic This looks great but we're currently not using babel since we're only supporting latest browsers. The solution @henriqemalheiros described above works almost perfectly for our use case, downloading the files only as necessary. It makes a request for each separate file, which isn't so bad since we're using HTTP2 and once the file is downloaded once it can be cached.

The only missing piece is that all the generated .js files are placed in the root webpack output directory. If some way could be added to specify a file pattern for the output files (as I've described in my last comment) so that we could customize the directory and file name then it would be perfect!

@rightaway you can already do that, actually:

return () => import(
  /* webpackChunkName: "icons/icon-[request]" */
  `./icons/${this.icon}.svg`
)

The [request] placeholder takes the file name, alongside its extension, and converts to a kebab-case string.

The extension on the chunk name could be annoying. Is possible to remove it by adding .svg to resolve.extensions on your Webpack configuration and then import just ./icons/${this.icon}.

@henriqemalheiros I've tried the example as you've written it and for some reason it's still putting those files in the root output directory rather than in an icons directory. In the webpack build output it says icons/icon-NAME-svg under the Chunk Names column, which is expected, but under the Asset column it has just the name of the file with no icons directory in front of it. Files that get placed in other directories will show the directory in the Asset column.

Though would it be possible though to implement it as options like some loaders take?

options: {
    name: 'icons/icon-[name]-[hash].[ext]',
},

So that it can be specified it in the webpack config file. Otherwise this particular path is hidden in the file containing the import while all other paths related to deployment are in the webpack config. And also having options could allow use of very useful [path], [contenthash], and other keys.

To clarify, when it loads the file the URL it loads is icons/icon-NAME-svg.js, but the files are still in the root output directory which isn't very clean since I have many icon files and this makes it difficult to find the few root level files (.js, .html, etc) that actually belong in the root directory.

A bigger issue is because the name icons/icon-NAME-svg.js doesn't contain [contenthash] this doesn't work with the forever caching strategy I use for all other assets based on the file name. For example if I change some svgo parameters causing the file content to change, the file name will remain the same.

So there needs to be a request to the server every time which would return a 304 not modified. Instead if I could use [contenthash] the browser could be instructed to cache it forever and not even need to make a request to the server.

If I take out the webpackChunkName comment then the file contains a hash in the name (as webpack does for all other files it generates using my config), but that leads to the original issue that all files are output in the root directory rather than a subdirectory.

So the names that webpack generates by default for these svg files are great, I just need to instruct it to put the files in [path] for example, so that if the file comes from images/category/icon-1.svg, it will be put in the webpack output directory as images/category/$CONTENTHASH.js. Could such a feature be possible to implement?

@rightaway please, check your Webpack configuration. I made a simple repo that does what you're asking: a separated chunk for each dynamically imported SVG with a hashed filename on a separated distribution folder. Just clone the repo, run npm run build and then check the dist folder.

As regards to adding options.name to this loader, I — personally — don't think is necessary. But you can create a new issue for this feature to be discussed since is not actually related to dynamically loading SVGs.

Hi, I'm using this method with Nuxt, thanks @henriqemalheiros for this simple repo/demo.
But I'm having some problem with old browsers as explained in this issue : https://github.com/visualfanatic/vue-svg-loader/issues/63

I tried the Nuxt solution provided in the 63 issue by @DigiSplit but it doesn't work with dynamic loading.
I'm currently doing :

 
config.module.rules
        .filter(r => r.test.toString().includes("svg"))
        .forEach(r => {
          r.test = /\.(png|jpe?g|gif)$/;
       });
      config.module.rules.push({
        test: /\.svg$/,
        loader: "vue-svg-loader"
      });

If somebody could help me with that or point me to the right direction it would be great. i already spent days on this without luck :/

@henriqemalheiros In this example you gave above

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

<script>
export default {
  name: 'DynamicIcon',
  props: {
    icon: {
      type: String,
      required: true,
    },
  },
  computed: {
    dynamicIcon () {
      return () => import(
        /* webpackChunkName: "icons" */
        /* webpackMode: "lazy-once" */ 
        `./icons/${this.icon}.svg`
      )
    },
  },
}
</script>

Is it possible to dynamically change the icon property and have the icon update automatically? It doesn't work for me, am I doing it wrong? When I run changeIcon below the data does update with the new value, but the icon doesn't change. How to do it?

<template>
  <DynamicIcon :icon="icon"/>
<template>

<script>
export default {
  data() {
    return {
      icon: "myicon1"
    }
  },
  methods: {
    changeIcon() {
      this.icon = "myicon2"
    }
  }
}
</script>

The dynamic switching works for me with require but not import, but unfortunately require doesn't let you use the magic comments for webpackChunkName. Any developers still active here? If so I'll provide a reproducible repo. Could really use a bit of help on this. :(

@rightaway This happens because the nature of require (synchronous) and import (asynchronous) it's different. Require in commonJs synchronously retrieve the exports from another module, import instead relies on Promise internally.
What this means?
Means that substantially if you use require, at build time the Vue variables are unknown to webpack, so the built will include all the svg files in the bundle and then uses only those that are really required. Consequently your bundle size and build time can be substantially bloated. Moreover, clearly, optimization techniques as Lazy loading etc are not gonna be really effective.
On the other side if instead you use import, you will be unable to take advantage of the reactivity, due to the sync vs async issue (the props changes but the computed props is not able to, because doesn't see the change inside the path).

To have a fully reactive computed property, in the case you are referring to, you need to make somehow the compute property aware of the change in the props. This is easily achieved just referencing the props before the return call.

Important, this technique will make re-compute the computed property every time the props changes (be aware of the behavior before implementing it, it opens up "mutation" on the computed property).

In order to achieve a fully dynamic reactive behavior, change your example in the following way:

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

<script>
export default {
  name: 'DynamicIcon',
  props: {
    icon: {
      type: String,
      required: true,
    },
  },
  computed: {
    dynamicIcon () {
      this.icon;  // NOTE THIS IS THE REFERENCE(it's enough to trigger props change)
      return () => import(
        /* webpackChunkName: "icons" */
        /* webpackMode: "lazy-once" */ 
        `./icons/${this.icon}.svg`
      )
    },
  },
}
</script>
Was this page helpful?
0 / 5 - 0 ratings

Related issues

santigarcor picture santigarcor  ·  8Comments

Mister-Hope picture Mister-Hope  ·  3Comments

sashakaralchuk picture sashakaralchuk  ·  6Comments

jpetko picture jpetko  ·  5Comments

edarioq picture edarioq  ·  6Comments