Managing SVG images in Vue.js applications

Managing SVG images in Vue.js applications
Managing SVG images in Vue.js applications. It’s hard to imagine a modern web application without SVG graphics in the form of icons, logos and other elements of the user interface

A few years ago the typical way of managing SVG images was to use special web fonts which served as collections of icons, for example glyphicons. But these fonts usually contain a lot of icons that you don’t need. Today, the best and most efficient approach is to simply insert SVG images inline into the HTML markup.

When you use a front-end framework like Vue.js, it’s important to realize that a piece of SVG is not any different than a piece of HTML markup. And since SVG images are isolated and reusable, they are no different than regular Vue components.

You can easily convert an SVG image to a single-file Vue component like this:

<template>
  <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    <path d="..."/>
  </svg>
<template>

Doing this manually for dozens of images isn’t very practical, but fortunately webpack can do this for us. There are a few available solutions, for example:

  • vue-svg-loader, which makes it possible to import .svg files directly as if they were Vue components;
  • vue-template-loader, which is intended to convert static .html files to Vue component templates, but is also works great with .svg files;

Personally I use vue-template-loader, because it’s more flexible.

Configuring webpack

First, we need to install the necessary modules:

$ npm install --save-dev vue-template-loader svgo-loader svgo

In a typical web application we will use both inline SVG images and external SVG images loaded using the <img> tag. I use the following approach:

  • Since inline SVG images are basically Vue components, I place them in the src/components/svg folder of the application.
  • All external SVG images are placed together with PNG, JPEG and other files in the src/images folder.

I will talk about why this separation is important later. First, let’s take a look at the following fragment of webpack configuration that tells it how to handle SVG files:

{
  test: /\.svg$/,
  rules: [ {
    oneOf: [ {
      resourceQuery: /^\?vue-template/,
      loader: 'vue-template-loader',
      options: {
        functional: true
      }
    }, {
      loader: 'file-loader',
      options: {
        name: 'images/[name].[ext]?[hash]'
      }
    } ]
  }, {
    loader: 'svgo-loader',
    options: {
      plugins: [
        { removeViewBox: false },
        { removeDimensions: true }
      ]
    }
  } ]
}

This rule is quite complex, so let’s analyze what it means. Remember that webpack loaders are executed in reverse order. So all .svg file are first processed by the svgo-loader, which we will talk about later.

What happens next is defined by a conditional rule. If the so called resource query contains the string ?vue-template, the file is processed by the vue-template-loader, which converts it to a JavaScript function. Otherwise, the standard file-loader is used, which copies the file to the output folder.

The resource query is simply a string that can be added to a file path in the require() or import statement, to let webpack know that it should be handled in a specific way:

import AddUser from './svg/AddUser.svg?vue-template';

This instructs webpack to compile the file using vue-template-loader. Without the query string, it would be processed by file-loader and the target path would be returned.

Global components

Manually importing every SVG image in such way isn’t great. It’s better to register all inline SVG images as global components. This way we can use them across the application without having to worry how to load them.

Once again, webpack can help us do it automatically. Let’s create an index.js file in the folder which contains all the inline SVG images:

import Vue from 'vue';
const requireTemplate = require.context( '.?vue-template', false,
  /\.svg$/ );
context.keys().forEach( fileName => {
  const name = 'Svg' + fileName.replace( /^\.\//, '' )
    .replace( /\.svg$/, '' ) );
  const withRender = requireTemplate( fileName );
  const component = withRender( {} );
  Vue.component( name, component );
} );

The require.context() function is very useful if you want to create an index file which automatically imports all files from a particular folder, without having to list them manually. You can read about it in the webpack documentation and the Vue.js guide.

The first parameter to require.context() indicates that we want to load files from the current folder . (i.e. the one containing the index.js file), using the ?vue-template resource query. We ignore subfolders and filter the files by the .svg extension.

Within the loop, we convert the file name, for example ./AddUser.svg, to a Vue component name, in this case SvgAddUser. Then we load the file, and the result is a function generated automatically by vue-template-loader, which can be used to create the actual Vue component. We finally register the component using Vue.component().

In order to register the SVG components, you should import the index.js file from the main file of your application, before creating the application instance.

Now you can insert the inline SVG images anywhere in your application:

<template>
  <button><SvgAddUser/> Add User</button>
</template>

Local components

If you want to avoid registering the components globally, you can use a slightly different approach and rewrite the index.js file in the following way:

const components = {};
const requireTemplate = require.context( '.?vue-template', false,
  /\.svg$/ );
context.keys().forEach( fileName => {
  const name = 'Svg' + fileName.replace( /^\.\//, '' )
    .replace( /\.svg$/, '' ) );
  const withRender = requireTemplate( fileName );
  components[ name ] = withRender( {} );
} );
module.exports = components;

This way you can import the components that you need and register them locally, for example:

<template>
  <button><SvgAddUser/> Add User</button>
</template>
<script>
import { SvgAddUser } from './svg';
export default {
  components: {
    SvgAddUser
  }
};
</script>

External SVG images

Whether you register the components globally or locally, all .svg files placed in the folder containing the index.js file will be converted to JavaScript code and included in the application bundle. So if you have some large or rarely used images that should be loaded externally, you should place them in another folder, as I mentioned earlier.

You can use the regular <img> tag to insert these external SVG images:

<template>
  <img src="../images/logo.svg">
</template>

Optimization and customization

We configured webpack to use svgo-loader for all SVG images — both the inline and external ones. This loader processes these images using svgo, which is essentially a powerful SVG optimizer. It can significantly reduce the size of the SVG markup by joining and simplifying paths and removing unnecessary information.

The default svgo configuration is quite reasonable, but to make it more suitable for inline SVG images, we configured it to remove the explicit width and height attributes and to preserve the viewport attribute. This way, we can specify the actual size of the SVG images using CSS.

For example, we can wrap the SVG icons using a <span> tag with a class:

<span class="icon"><SvgAddUser/></span>

This allows us to customize the size of the icon like this:

.icon {
  display: inline-block;
}
.icon > svg {
  display: block;
  width: 1em;
  height: 1em;
}

In a similar way, we can also customize the color of the icons depending on various conditions, for example:

.icon > svg {
  fill: #000;
}
.icon.icon-green > svg {
  fill: #0f0;
}

You can do more advanced operations with svgo, for example optimize the images even more aggressively by reducing the precision of coordinates. Consult the svgo documentation for more information.

Summary

Converting SVG icons to reusable Vue.js components makes it possible to create great looking and optimized applications. Thanks to webpack and its powerful loaders, it can be done in a fully automated way with just a few lines of code, so you never have to think about it again.

Recommended Reading

Deploying Nuxt.js SSR Apps To AWS ECS

Built a Progressive Web App with Vue.js and Ionic 4

How to Build Feedback Application with Vue.js

Suggest:

Is Vue.js 3.0 Breaking Vue? Vue 3.0 Preview!

Learn Vue.js from scratch 2018

Vue.js Tutorial: Zero to Sixty

Learn Vue.js from Scratch - Full Course for Beginners

JavaScript Programming Tutorial Full Course for Beginners

Create Shopping basket page With Vuejs and Nodejs