jpkleemans / vite-svg-loader

Vite plugin to load SVG files as Vue components
MIT License
585 stars 61 forks source link

How to have a dynamic import SVG component #66

Closed SirMishaa closed 2 years ago

SirMishaa commented 2 years ago

Hello,

I have an issue when building the Vue (2.7) app:

[plugin:vite:dynamic-import-vars] invalid import "/icons/${this.name}.svg?raw". Variable absolute imports are not supported, imports must start with ./ in the static part of the import. For example: import(./foo/${bar}.js).

We're using this component everywhere in our application. SwIcon.vue :

<template lang="pug">
  div
    span.block(
      :class="[styleClass, sizeClass]"
      :style="{strokeLinecap: 'round', strokeLinejoin: 'round'}"
      @click="$emit('click')"
      v-html="svgRawContent"
    )
</template>

<script>
export default {
  name: 'SwIcon',
  props: {
    name: {
      type: String,
      default: '',
    },
    unstyled: {
      type: Boolean,
      default: false,
    },
    size: {
      type: String,
      default: 'w-6 h-6',
    },
  },
  data () {
    return {
      svgRawContent: '',
    };
  },
  computed: {
    sizeClass () {
      return this.size;
    },
    styleClass () {
      return this.unstyled ? '' : 'fill-none stroke-current stroke-2';
    },
  },
  // Todo: Replace by Suspense or something else when upgrading to Vue 3 to avoid async lifecycle method
  async mounted () {
    const svgObject = await import(`/icons/${this.name}.svg?raw`);
    this.svgRawContent = svgObject.default;
  },
};
</script>

Like you can see, we have this import await import(`/icons/${this.name}.svg?raw`); which is working good in development.

I guess the problem comes from the fact that during the build phase, since it is dynamic, it is not able to bundle the svg since it does not know them.

But then, how can I keep a similar component and make it work? Even if it has to bundle the whole icon's folder, it's not a big deal if it's lazy loaded.

Thanks in advance, I appreciate it ❤️

PS: I have already check #24, but I don't understand well for my case :/

SirMishaa commented 2 years ago

Folder architecture is the following : image

Basically, SVG component is here : frontend/components/swComponents/SwIcon.vue Svg are in this folder: frontend/icons but we have some icons in subfolder, so frontend/car_discounts for example. (I think, that's maybe the tricky part)

jpkleemans commented 2 years ago

Hi, does it work when you use a relative path?

await import(`../../icons/${this.name}.svg`)
SirMishaa commented 2 years ago

Hi, does it work when you use a relative path?

await import(`../../icons/${this.name}.svg`)

No, I got the following error message :

Unknown variable dynamic import: ../../icons/car_discounts/car_lane_departure.svg

I think It's because the "name", is not a name, but sometime a path + name (yeah, bad naming, we're doing a lot of refactorings 😅)

jpkleemans commented 2 years ago

Unknown variable dynamic import: ../../icons/car_discounts/car_lane_departure.svg

It seems that the file can't be found. Are you sure icons/car_discounts/car_lane_departure.svg exists?

SirMishaa commented 2 years ago

image

Yep : frontend/icons/car_discounts/car_lane_departure.svg (My IDE generate the path, and I've double-checked)

I'm not sure the error message mean that, I don't know where does it come from

SirMishaa commented 2 years ago

Any idea? :(

jpkleemans commented 2 years ago

Can you try to use defineAsyncComponent to see if that works?

import { defineAsyncComponent } from 'vue'

export default {
  mounted () {
    const svgObject = defineAsyncComponent(() => import(`/icons/${this.name}.svg?raw`))
  }
}
adrianrivers commented 2 years ago

I'm using this code to render icons from a subfolder dynamically

<template>
  <IconComponent  :viewBox="viewBox" />
</template>

<script setup lang="ts">
import type { Component } from 'vue'
import { IconName } from '@/types/IconName'

export interface IconProps {
  name: IconName
  fill?: Color
  viewBox?: string | null
}

const props = withDefaults(defineProps<IconProps>(), {
  fill: 'default',
  viewBox: null,
})

const { loader } = createIconMap().get(props.name) ?? {}
const IconComponent = loader ? defineAsyncComponent(loader) : null

function getIconNameFromPath(path: string) {
  const pathSplit = path.split('/')
  const filename = pathSplit[pathSplit.length - 1] || ''
  const iconName = filename.replace('.svg', '')

  return iconName
}

function createIconMap() { 
  const importGlob = import.meta.glob('@/assets/icons/**/*.svg')
  const iconMap = new Map<string, { loader: () => Promise<Component> }>([])

  for (const path in importGlob) {
    const iconName = getIconNameFromPath(path)
    iconMap.set(iconName, { loader: importGlob[path] })
  }

  return iconMap
}
</script>

Sorry I haven't worked much with Vue 2 but I'm sure something for Vue 2 could be adapted from the code above.