jpkleemans / vite-svg-loader

Vite plugin to load SVG files as Vue components
MIT License
554 stars 58 forks source link

Having trouble dynamic import SVG #85

Open Jiaocz opened 1 year ago

Jiaocz commented 1 year ago

I've set the default import to url in vite.config.ts

svgLoader({
  defaultImport: 'url',
}),

And then when I'm using dynamic import for a certain icon, it will cause an error like:

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

<script setup lang="ts">
import { defineAsyncComponent } from "vue"

const props = defineProps<{ name: string }>()
const icon = defineAsyncComponent(() => import(`@/assets/svg/${props.name}.svg?component`))
</script>
Uncaught (in promise) Error: Invalid async component load result: /src/assets/svg/vue.svg

Is there any solution for this situation?

acicero93 commented 1 year ago

@Jiaocz Did you ever find a solution?

Jiaocz commented 1 year ago

@Jiaocz Did you ever find a solution?

Nope, we've temporarily set the defaultImport to component and when using SVG within <img> we manually add a ?url suffix.

Maybe the import() function doesn't recognize that file path suffix.

yooouuri commented 1 year ago

I guess I found why this won't work, because the @ doesn't get honoured in https://github.com/jpkleemans/vite-svg-loader/blob/main/index.js#L30, am I right @jpkleemans ?

yooouuri commented 1 year ago

@Jiaocz found the issue, don't set defaultImport to url, see: https://github.com/jpkleemans/vite-svg-loader/blob/main/index.js#L21-L25

The ?component is ignored...

const icon = defineAsyncComponent(() => import(`../../assets/img/icons/${props.name}.svg?component`))
<Component :is="icon" />

Is working for me.

Im going to make a PR, to fix this.

yooouuri commented 1 year ago

Hmm weirdly, if you log id the async load (id) function, ?component is removed from the path... Don't know if this intentionally by Vite or not. But because of the component not present it always fallback to the default value in the settings

acicero93 commented 1 year ago

@Jiaocz found the issue, don't set defaultImport to url, see: https://github.com/jpkleemans/vite-svg-loader/blob/main/index.js#L21-L25

The ?component is ignored...

const icon = defineAsyncComponent(() => import(`../../assets/img/icons/${props.name}.svg?component`))
<Component :is="icon" />

Is working for me.

Im going to make a PR, to fix this.

Strange, still doesn't work for me. I should have mentioned I'm trying to build using library mode, maybe thats the issue?

BenjaminOddou commented 1 year ago

I have a quite similar code as @Jiaocz but working on some situations. I created a vue component TheSVG.vue like so

<script setup lang="ts">
const props = defineProps<{ name: string }>()
const icon = defineAsyncComponent(() => import(`../assets/svgs/${props.name}.svg?component`))
</script>

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

in order to dynamically import my svgs into my pages. The problem is that the element is not available under the onMounted hook and I don't know why.

// SomePage.vue
<script setup lang="ts">
onMounted(() => {
  console.log(document.querySelector(
    '#big-circle > ellipse'
  )) // returns null after page transition. Hard reload is working
})
</script>

<template>
      <TheSVG id="big-circle" name="big-circle" />
</template>

However importing the svg directly (without dynamic import is working fine.

// SomePage.vue
<script setup lang="ts">
import bigCircle from '/assets/svgs/big-circle.svg?component'

onMounted(() => {
  console.log(document.querySelector(
    '#big-circle > ellipse'
  )) // Is always working
})
</script>

<template>
      <bigCircle id="big-circle" />
</template>

FYI I am using Nuxt3 with SSR. My nuxt.config.ts :

import svgLoader from 'vite-svg-loader'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  vite: {
    plugins: [
      svgLoader({
        svgo: false,
      }),
    ],
  }
})

If you have any idea don't hesitate ! Many thanks in advance :smile:

gkatsanos commented 1 year ago

Hey guys, if you could post a minimal repro link on stackblitz that'd be awesome, there's several messages in this issue but I think they may not be the same thing.

Alex-Kozynko commented 10 months ago

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

<script>
import { defineAsyncComponent } from 'vue'

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

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

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

<template>
  <component :is="icon" :class="className" />
</template>
Luetzen commented 10 months ago

@Alex-front-end-developer

Hey, I tried your solution but it doesn´t work for me.

I got some issues to call the function:

I debuged like following:

icon() {

  console.log(this.icons); --> Object
  console.log(this.name); --> I got the name of the icon

  const icon = this.icons[this.name];

  console.log(typeof icon);  --> undefined Uncaught (in promise) TypeError: this.icons[this.name] is not a function
  return defineAsyncComponent(() => icon());
},

Can you help me out here? No idea whats going wrong.
Alex-Kozynko commented 10 months ago

@Luetzen please debug your "icons" object.

Luetzen commented 10 months ago

@Alex-front-end-developer

Thanks for the hint! I took the worng path.

icons: import.meta.glob(@/assets/icons/*.svg), seems better

image

Not it seems icons is filled but still the same error. Uncaught (in promise) TypeError: this.icons[this.name] is not a function

Any idea whats going on?

Alex-Kozynko commented 10 months ago

@Luetzen replace this.icons[this.name] with this.icons[`/src/assecc/icons/${this.name}.svg`], or pass the full path to the icon in the name. You have element keys in the object, they must match when calling the function, otherwise the element simply does not exist.

codiini commented 10 months ago

@Alex-front-end-developer I'm having similar issues as well. here's my component (Vue 3)

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

<script lang="ts" setup>
import { defineAsyncComponent, computed } from "vue";

const props = defineProps({
  name: {
    type: String,
    required: true,
  },
});

const iconList = import.meta.glob(`@/assets/icons/**/*.svg`);

console.log(iconList);

const Icon = computed(() => {
  return defineAsyncComponent(() =>
    iconList[`../../assets/icons/svg/${props.name}.svg`]()
  );
});
</script>

And here's the content of my iconList

image

But I also get the iconList[props.name] is not a function error

Luetzen commented 10 months ago

@Alex-front-end-developer

Solution from this thread

I tested your solution. Now I get to the same error like in other examples to create svg as component (take a look on "Another Solution").

runtime-core.esm-bundler.js:2337 Uncaught (in promise) Error: Invalid async component load result: /src/assets/icons/bars-solid.svg

//IconStandard.vue

<script lang="ts">
import { defineAsyncComponent } from "vue";

export default {
  props: {
    name: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      icons: import.meta.glob(`@/assets/icons/*.svg`),
    };
  },
  computed: {
    icon() {
      console.log(this.icons);
      return defineAsyncComponent(() => this.icons[`/src/assets/icons/${this.name}.svg`]());
    },
  },
};
</script>

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

//App.vue

...
<IconStandard name="bars-solid" />
...

//vite.config.js

....
  plugins: [
    vue(),
    svgLoader({
      defaultImport: "url", // or 'url' or 'component'
      svgoConfig: {
        multipass: true,
      },
    }),
  ],

Another Solution

//IconStandard.vue

<script setup>
import { defineAsyncComponent } from 'vue';

const props = defineProps({
  name: {
    type: String,
    required: true,
  },
});

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

<template>
  <component :is="icon" class="fill-current" />
</template> 

//App.vue

<template>
  <nav>
    <div navigation__links>
      <icon name="bolt-solid"></icon>
    </div>
</template>

//Vite.Config

....
  plugins: [
    vue(),
    svgLoader({
      defaultImport: "component", // or 'url' or 'component'
      svgoConfig: {
        multipass: true,
      },
    }),
  ],
});

runtime-core.esm-bundler.js:2337 Uncaught (in promise) Error: Invalid async component load result: /@fs/assets/icons/bolt-solid.svg at runtime-core.esm-bundler.js:2337:31

It is not the same error but similiar I think.

Alex-Kozynko commented 10 months ago

@Luetzen I'm amazed at your carelessness, your default import is the url in the first example, but the component should be :)

Luetzen commented 10 months ago

Shame on me. I thought I had tried that as well, was even pretty sure. I must have missed it though.

Now it works.

Here again for others who may have had the same problem as me.

Using the Component

    <IconStandard name="bolt-solid" />

SVG Component

<script>
import { defineAsyncComponent } from "vue";

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

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

  computed: {
    icon() {
      console.log(this.icons);
      return defineAsyncComponent(() => this.icons[`/src/assets/icons/${this.name}.svg`]());

    },
  },
};
</script>

<template>
  <component :is="icon" :class="className" />
</template>
// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import svgLoader from "vite-svg-loader";

// https://vitejs.dev/config/
...
export default defineConfig({
  plugins: [
    vue(),
    svgLoader({
      defaultImport: "component", // or 'url' or 'component'
      svgoConfig: {
        multipass: true,
      },
    }),
  ],
});

Thanks a lot to @Alex-front-end-developer !

Alex-Kozynko commented 10 months ago

@codiini iconList is an object, so when you call its property, you need to enter the key, not the path, please correlate with the keys of the object on your screenshot and you will understand what's going on :)

Alex-Kozynko commented 10 months ago

@Jiaocz I think you can close the question, and post the last message from @Luetzen as an answer, it is quite detailed.

codiini commented 10 months ago

@Alex-front-end-developer I copied the solution from @Luetzen and I'm now getting this error Invalid async component load result: /_nuxt/assets/icons/svg/app-logo.svg and this is my vite.config.js file

/** @type {import('vite').UserConfig} */
import svgLoader from "vite-svg-loader";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    vue(),
    svgLoader({
      defaultImport: "component",
      svgoConfig: {
        multipass: true,
      },
    }),
  ],
});

Any idea what the issue might be? I'd appreciate the help. Thank you

Alex-Kozynko commented 10 months ago

@codiini check what keys you have in the icons object and match them with the key call, the keys must match.

codiini commented 10 months ago

@Alex-front-end-developer

<script>
import { defineAsyncComponent } from "vue";

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

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

  computed: {
    Icon() {
     console.log(this.icons);
      console.log(this.icons[`/assets/icons/svg/${this.name}.svg`]);
      // return defineAsyncComponent(() =>
      //   this.icons[`/assets/icons/svg/${this.name}.svg`]()
      // );
    },
  },
};
</script>

<template>
  <component :is="Icon" class="fill-current"></component>
</template>

Here's the result of the both console.log The keys match and return the correct imports.

image
Alex-Kozynko commented 10 months ago

@codiini try to console what returns after the call.

codiini commented 10 months ago

@Alex-front-end-developer So, I'm wondering whether it's an issue with using Nuxt and not Vue. I tried the same code with a Vue app, and it worked.

I also tried just importing the SVG as a component in the Nuxt app like this

<script lang="ts" setup>
import AppLogo from "../assets/icons/svg/app-logo.svg";
</script>

<template>
<AppLogo />
</template>

And I got the following error

image
alexmccabe commented 5 months ago

So the accepted workaround / solution for me didn't cut the mustard. I needed to set the defaultImport: 'url' and keep dynamic imports. Everything outlined above did not work.

Instead, I bypassed the plugin entirely, which allowed me to remove it. Yes it's a little bit more manual work, however you get full control over everything.

function createAsyncComponent() {
    const importPromise = new Promise((resolve, reject) => {
        // Obviously change this to the path to your icon file location
        import(`./icons/${props.iconName}.svg?raw`)
            .then((mod) => resolve({ template: mod.default, name: `${props.iconName}Icon` }))
            .catch((error) => reject(error));
    });

    return defineAsyncComponent(() => importPromise);
}

const IconComponent = computed(() => {
    // Perform any validation you need to make sure that it's a valid icon
    const valid = validateIconProps();

    if (!valid) return null;

    return createAsyncComponent();
});

After this, the plugin became unnecessary for our project. But I thought that it might be valuable for people who arrive here in the same position as me.

syahrizalxs commented 3 months ago

I've set the default import to url in vite.config.ts

svgLoader({
  defaultImport: 'url',
}),

And then when I'm using dynamic import for a certain icon, it will cause an error like:

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

<script setup lang="ts">
import { defineAsyncComponent } from "vue"

const props = defineProps<{ name: string }>()
const icon = defineAsyncComponent(() => import(`@/assets/svg/${props.name}.svg?component`))
</script>
Uncaught (in promise) Error: Invalid async component load result: /src/assets/svg/vue.svg

Is there any solution for this situation?

Have you found the solution? I got same issue here

Cyapow commented 2 months ago

Shame on me. I thought I had tried that as well, was even pretty sure. I must have missed it though.

Now it works.

Here again for others who may have had the same problem as me.

Using the Component

    <IconStandard name="bolt-solid" />

SVG Component

<script>
import { defineAsyncComponent } from "vue";

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

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

  computed: {
    icon() {
      console.log(this.icons);
      return defineAsyncComponent(() => this.icons[`/src/assets/icons/${this.name}.svg`]());

    },
  },
};
</script>

<template>
  <component :is="icon" :class="className" />
</template>
// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import svgLoader from "vite-svg-loader";

// https://vitejs.dev/config/
...
export default defineConfig({
  plugins: [
    vue(),
    svgLoader({
      defaultImport: "component", // or 'url' or 'component'
      svgoConfig: {
        multipass: true,
      },
    }),
  ],
});

Thanks a lot to @Alex-front-end-developer !

Late to the game here but this sorted it for me. I have trimmed the code down a bit if anyone wants it. I had to use the path from the root of the project rather than relative path.

<script setup>
import { defineAsyncComponent, defineProps, computed, useAttrs } from "vue";

const props = defineProps({
  name: {
    type: String,
    required: true
  }
});

const attrs = useAttrs();

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

<template>
  <component :is="icon" v-bind="attrs" />
</template>
m-shum commented 1 month ago

None of these solutions are working for me.

Importing every SVG in the public directory isn't an option as we have 100s.

Dynamically importing them yields the following error, however: Error: Invalid async component load result: /_nuxt/@fs/__skip_vite/public/svg/icons/caret-down.svg

Here's what I'm working with:

<script setup>
const props = defineProps({
  name: {
    type: String,
    default: '',
  },
  fill: {
    type: String,
    default: '',
  },
  stroke: {
    type: String,
    default: '',
  },
})

const filePath = computed(() => {
  const dirs = segmentFileName(/(\w+)\//gm)
  const fileName = segmentFileName(/\S+\/(\S+$)/gm)
  if (!fileName.length) return
  let basePath = '/public/svg'
  if (dirs.length) {
    basePath = basePath + dirs.reduce((path, dir) => `/${dir}`, '')
  }
  return `${basePath}/${fileName[0]}.svg`
})

function segmentFileName(regex) {
  const matches = []
  let m

  while ((m = regex.exec(props.name)) !== null) {
    if (m.index === regex.lastIndex) {
      regex.lastIndex++
    }
    m.forEach((match, groupIndex) => {
      if (groupIndex === 1) matches.push(match)
    })
  }
  return matches
}

const svgComponent = computed(() =>
  defineAsyncComponent(() => import(filePath.value)),
)
</script>

<template>
  <div class="svg-wrapper">
    <component :is="svgComponent" />
  </div>
</template>

And in nuxt.config:

import svgLoader from 'vite-svg-loader'
export default defineNuxtConfig({
  ...
  vite: {
    plugins: [svgLoader()],
    },
   ...
 }),

I'm genuinely surprised that this is still a problem 2 years later. Am I adding the svgLoader plugin incorrectly?