Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically refer to an icon #51

Closed
rightaway opened this issue Oct 8, 2018 · 16 comments
Closed

Dynamically refer to an icon #51

rightaway opened this issue Oct 8, 2018 · 16 comments

Comments

@rightaway
Copy link

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?

@henriqemalheiros
Copy link
Contributor

You can use dynamic components.

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

@rightaway
Copy link
Author

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?

@wildstyles
Copy link

wildstyles commented Oct 18, 2018

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;
}

@henriqemalheiros
Copy link
Contributor

henriqemalheiros commented Oct 18, 2018

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

Since 0.11.0 (1fa688d, damianstasik/svg-to-vue@151c5d7), 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>

@rightaway
Copy link
Author

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?

@damianstasik
Copy link
Owner

@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.

@rightaway
Copy link
Author

@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!

@henriqemalheiros
Copy link
Contributor

@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}.

@rightaway
Copy link
Author

@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.

@rightaway
Copy link
Author

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?

@henriqemalheiros
Copy link
Contributor

@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.

@desaintflorent
Copy link

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 : #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 :/

@rightaway
Copy link
Author

@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>

@rightaway
Copy link
Author

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. :(

@Dinuz
Copy link

Dinuz commented Apr 12, 2020

@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>

@petitkriket
Copy link

petitkriket commented Mar 18, 2022

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; }

same here, adding .default at the end of require() solved the issue I had when migrating to vue 3 while prodividing the value to a dynamic component

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants