Search code examples
vue.jswebpackvuejs2vue-componentstorybook

Storybook does not load SVGs in Components of Vue project


I have created a Vue project with vue-cli and installed storybook later via npm install --save-dev storybook as a dependency to show my created components.

The storybook webpack config looks like this:

const path = require('path')

module.exports = async ({ config, mode }) => {
  config.module.rules.push(
    {
      test: /\.svg$/,
      use: ['vue-svg-loader']
    },
    {
      test: /\.scss$/,
      use: [
        'style-loader',
        'css-loader',
        {
          loader: 'sass-loader',
          options: {
            sourceMap: false,
            data: '@import "./src/assets/styles/main.scss";'
          }
        }
      ],
      include: path.resolve(__dirname, '../')
    }
  )

  return config
}

and the story index.js like this:

import Vue from 'vue';
import { storiesOf } from '@storybook/vue';
import Alert from '../src/components/buttons/Alert.vue';

storiesOf('Components', module)
  .add('Alert', () => ({
    components: { Alert },
    template: '<Alert />'
  }))

for some reason when trying to load a component which consists of a SVG I'm getting:

enter image description here

The component itself is displayed but the part where the SVG sits no SVG is showing.

Interestingly though it works just fine when trying to display the component in the Vue's main app. The vue.config looks like this:

module.exports = {
  css: {
    loaderOptions: {
      sass: {
        data: `@import "./src/assets/styles/main.scss";`
      }
    }
  },
  chainWebpack: config => {
    const svgRule = config.module.rule("svg");

    svgRule.uses.clear();

    svgRule.use("vue-svg-loader").loader("vue-svg-loader");
  }
};

Why does storybook does not load the svgs properly while the main Vue app can?

Edit: I have gone ahead and simply used webpacks file-loader too to make sure it has nothing to do with vue-svg-loader:

{
  test: /\.(png|jpg|gif|svg)$/,
  use: [
    {
      loader: 'file-loader',
      options: {},
    },
  ],
},

but I am getting the same error. After trying to apply the first answer to replace push() to rules with unshift() I am getting this error: Error in parsing SVG: Non-whitespace before first tag.

Edit2:

This is one of the SVGs I'm trying to load:

<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exclamation-triangle" class="svg-inline--fa fa-exclamation-triangle fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"></path></svg>

Edit3: *Alert.vue in projectroot/src/components/banners/Alert.vue, Webpack config of Storybook is in projectroot/.storybook/webpack.config.js

<template>
  <div v-if="show" :class="`alert_wrap ${model}`">
    <IconBase>
      <component :is="iconComponent" />
    </IconBase>
    <div class="alert_text">
      <p v-if="title">{{ title }}</p>
      <p>{{ msg }}</p>
    </div>
    <IconBase>
      <TimesIcon @click="show = !show" aria-hidden="Close" />
    </IconBase>
  </div>
</template>

<script>
import IconBase from "../icons/IconBase.vue";
import CautionIcon from "../../assets/icons/exclamation-triangle-solid.svg";
import InfoIcon from "../../assets/icons/info-circle-solid.svg";
import SuccessIcon from "../../assets/icons/check-circle-solid.svg";
import TimesIcon from "../../assets/icons/times-solid.svg";

export default {
  name: "Alert",
  components: {
    CautionIcon,
    InfoIcon,
    SuccessIcon,
    TimesIcon,
    IconBase
  },
  props: {
    msg: {
      type: String,
      required: true
    },
    title: {
      type: String
    },
    model: {
      type: String,
      default: "info",
      validator(type) {
        return ["caution", "info", "success"].includes(type);
      }
    }
  },
  computed: {
    iconComponent() {
      return `${this.model}-icon`;
    }
  },
  data() {
    return {
      show: true
    };
  }
};
</script>

<style scoped lang="scss">
  /* some styles depending on the props passed */
</style>

Solution

  • @liximomo's answer is almost correct. You should remove svg file from file-loader processing rule, but the rule search condition should be slightly different:

    module.exports = ({ config }) => {
    
        let rule = config.module.rules.find(r =>
            // it can be another rule with file loader 
            // we should get only svg related
            r.test && r.test.toString().includes('svg') &&
            // file-loader might be resolved to js file path so "endsWith" is not reliable enough
            r.loader && r.loader.includes('file-loader')
        );
        rule.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/;
    
        config.module.rules.push(
            {
                test: /\.svg$/,
                use: ['vue-svg-loader']
            }
        )
    
        // ...
    
        return config;
    }
    

    Another way to import SVG without changing the Webpack config rules is to specify an inline loader, which can be useful to avoid errors when some components already use SVG in CSS or HTML as just a file path. Example:

    // disable all rules with !! and use just vue-svg-loader
    import CautionIcon from "!!vue-svg-loader!../../assets/icons/exclamation-triangle-solid.svg";