Rune Laenen

Shopware plugin manufacturer & freelancer

Custom icons in Shopware 6 Administration

24/05/2021

Shopware is very extendable, but not everything is as easy to do or is as well documented as we would sometimes like. To extend this online ‘community-documentation’, I like to write down some tips, tricks and code snippets and share them with the world. Today, I tried to add a custom icon to the Shopware 6 Administration, and found out that it was not very easy to do. Until you know how, of course!

In short…

I assume you already have your plugin with some administration code. This could be a custom module, or a change to an existing module. We will be extending the webpack config, we will also need to actually load our svg’s to be usable in the sw-icon component.

I assume you will put your svg icons in src/Resources/app/administration/src/app/assets/icons/svg, which mimics the path the icons are in in the core. The sw-icon component from the Shopware core dictates a naming convention for your file names. They need to start with icons-vendor-icon-name.svg, for example icons-rune-icon.svg. This will make it possible to load our icon with <sw-icon name="rune-icon"></sw-icon> in our code.

Tutorial

1. Extending webpack.config.js

Luckily, Shopware provides a way to extend the webpack config from a plugin. This happens with a config file, which the build scripts from Shopware will automatically pick up, after which it applies its config.

To get started, create a file `src/Resources/app/administration/build/webpack.config.js.

First of all, lets start with this small boilerplate code. This imports the needed dependencies, and exports a function which will extend the default config.

const { resolve, join } = require('path');

module.exports = ({ config }) => {
    // Add some magic here
};

Our end goal, and the reason why we need to extend the webpack config, is to include our svg icons inline. To do this, there are 2 things we need to do. First we need to remove (or ‘exclude’) our svg’s path from a certain rule (the url-loader rule which will make a base64 string, which we don’t want), and then add our path to another rule (the svg-inline-loader rule which… well loads svgs inline :-P).

For the first path, we will need to find the url-loader rule.

// Find the url loader rule
const urlLoaderRule = config.module.rules.find((rule) => {
    return rule.loader === 'url-loader';
});

When we found the rule, we will add our directory to the excludes parameter of the rule.

// Add our svg icons
urlLoaderRule.exclude.push(
    resolve(join(__dirname, '../src/app/assets/icons/svg'))
);

Finally, we add a new svg-inline-loader rule to the configuration.

return {
    module: {
        rules: [
            {
                test: /\.svg$/,
                include: [
                    resolve(join(__dirname, '../src/app/assets/icons/svg'))
                ],
                loader: 'svg-inline-loader',
                options: {
                    removeSVGTagAttrs: false
                }
            },
        ]
    }
};

So quick recap: We change the existing config (by adding something to an existing rule), and add some more config by adding the new rule. Here’s the complete file:

const { resolve, join } = require('path');

module.exports = ({ config }) => {
    // Find the url loader rule
    const urlLoaderRule = config.module.rules.find((rule) => {
        return rule.loader === 'url-loader';
    });

    // Add our svg icons
    urlLoaderRule.exclude.push(
        resolve(join(__dirname, '../src/app/assets/icons/svg'))
    );

    return {
        module: {
            rules: [
                {
                    test: /\.svg$/,
                    include: [
                        resolve(join(__dirname, '../src/app/assets/icons/svg'))
                    ],
                    loader: 'svg-inline-loader',
                    options: {
                        removeSVGTagAttrs: false
                    }
                },
            ]
        }
    };
};

2. Load the svg’s as a Vue component

The way the Shopware icons work, is by creating every icon as a Vue component. We won’t be making a component manually. You could, but that’s a lot of boiler plate copy-paste code, so we reuse the generator that Shopware core uses.

Create the following file src/Resources/app/administration/src/app/assets/icons/icons.js in your plugin:

export default (() => {
    const context = require.context('./svg', true, /svg$/);

    return context.keys().reduce((accumulator, item) => {
        const componentName = item.split('.')[1].split('/')[1];

        const component = {
            name: componentName,
            functional: true,
            render(createElement, elementContext) {
                const data = elementContext.data;

                return createElement('span', {
                    class: [data.staticClass, data.class],
                    style: data.style,
                    attrs: data.attrs,
                    on: data.on,
                    domProps: {
                        innerHTML: context(item),
                    },
                });
            },
        };

        accumulator.push(component);
        return accumulator;
    }, []);
})();

What this file does is the following:

  • Create a Node.js context object for the svg folder. This is the folder that holds your svg files.
  • Loop over the files, and for every file:
    • ‘Render’ the filename to get the component name. In short, it takes the filename and cuts off the .svg file extensions. This makes the ‘raw’ filename the component name.
    • Then it describes the component, and how it should be rendered: a span, with cetain classes, stles, attributes, and most importantly: the icon itself, which is loaded with the context(item) function.
    • Finally, we push the component to our list and return it.

It’s not just done yet. This file will not be automagically called, so we still have to import & run the file. To do this, open your main.js file and add the following:

import iconComponents from './app/assets/icons/icons';

iconComponents.map((component) => {
    return Shopware.Component.register(component.name, component);
});

This imports our icons.js file, runs it to generate the components, and registers every component to the Shopware component registry.

3. That’s it!

Now that I write it here, it’s actually quite simple. 3 files and you can get started with your own custom icons. You can now use the sw-icon component to include your shiny custom icons.

Troubleshooting

Are your icons still not loading? Maybe you have one of the issues I encountered:

1. Wrong file name

Make sure your icon components (= the svg file name) start with icons-. This is a prefix that is used by the sw-icon component. Filename icons-rune-test.svg will become <sw-icon name="rune-test"></sw-icon>

2. Wrong webpack.config.js

Are you seeing a base64 string? This means something is wrong with the webpack config, and your custom config is probably not loading correctly. Did you dump the plugin configuration? No? Run bin/console bundle:dump, restart your watcher/build and it should work. Are you sure you have restarted your watcher? No? Restart bin/watch-administration.sh and it should work.

Enjoy!

I hope nobody needs to spend as much time as I did to figure a simple thing like this out. If you were helped by this guide, consider buying me a coffee. Did you already figure this out yourself, and are you looking for a job? Contact me!