Advanced VueMaterial Theming
Tags: vue, vuematerial, javascript, theme
Published: 2020-02-19 20:59:43

###### Why? Well, ["Coming soon..."](https://vuematerial.io/themes/advanced) isn't soon enough.

### Disclaimer
This was the solution I had to come up on the spot. It serves its propose and can certainly be improved. It's based on old-time notions of "provide the minimum, download what you need".

## VueMaterial and Themes
It ain't easy, but I'll give you a summary. VueMaterial "native" theming is enough if all you want is to change some colors on the default theme and you should read [their configurations documents](https://vuematerial.io/themes/configuration) if all you want is that. 

Summarizing, you use the scss to provide some modifications to the "default" theme provided by vue-material which is then imported by your main file via your equivalent of
```js
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'
```

These are then caught by the corresponding webpack loaders and then spat out onto files and retrieved when needed.

## Intermedium Theming
But what if you want to provide the same functionality offered on vue-material website where you can change your theme on the fly?

Well, you'd need to add a new theme file, and then import it again on your main file, which would then represented on your final index.html. This is all cool until the following hits you: Each vue-material theme we produce has all the vue-material theming attached, courtesy of these two imports

```scss
@import "~vue-material/dist/theme/engine"; // Import the theme engine
@import "~vue-material/dist/theme/all"; // Apply the theme
```

Since you'll be repeating this throughout your themes, your site will get duplicated css that might, or probably will never, be used.

## Advanced Theming
How do we solve this? with a couple of preperation steps and a Singleton acting as a bridge between your application and the loading of new themes.

##### What we will be doing
We will need to hook on two life-cycles of a vuejs application: its serve and its build, and will act before and after, accordignly, with some actions that will extract the themes into the same folder that vuejs will output the website.

##### What you'll need
Issue the following so we deal with all dependencies in one go,
```
npm i -D glob clean-webpack-plugin remove-files-webpack-plugin optimize-css-assets-webpack-plugin cssnano file-loader extract-loader css-loader sass-loader node-sass webpack
```

### Themes structure
We will start by changing the main file and remove the inclusion of `import 'vue-material/dist/theme/default.css'` as we will have this be loaded later when the application starts

Following that, we will create a folder for our themes and a main one with some variables:
- create `/themes/`folder on the same level as `/src/`
- add a new `/main/`folder for the main theme
- and `variables.scss` and `theme.scss`

Populate `variables.scss` with
```scss
$theme-name: 'main' !default;
$primary-color: pink !default;
$secondary-color: blue !default;
$danger-color: red !default;
```

and `theme.scss` with
```scss
@import "~vue-material/dist/theme/engine";
@import "variables";

@include md-register-theme(
                $theme-name,
                (
                        primary: $primary-color,
                        accent: $secondary-color,
                        theme: light,
                        red: $danger-color
                )
)

:root {
  --md-theme-#{$theme-name}-custom-variables: pink;
}

.md-theme-#{$theme-name} {
  #app {
    font-family: monospacef;
  }
  
  /* your css customizations here, I'd advise you to make barrel-imports */
  @import "./import-barrel";
}

@import "~vue-material/dist/theme/all;
```

### Creating new themes
All we really need to create a new theme is override the values in `/themes/main/variables.scss` with the ones from the new theme,

create a new folder under `/themes/`with the name of the theme, `/theme/red-on-black/`, and create a `theme.scss` inside with

```scss
$theme-name: 'red-on-black';
$primary-color: 'red';
$secondary-color: 'black';
$danger-color: 'yellow';

@import '../main/theme.scss';
```

This will essentially make a copy of main theme with new values, since we provided `!default` on each value under `/themes/main/variables.scss` these will not override the variables provided by `/themes/red-on-black/theme.scss`

["A png is worth 10k chars"](https://i.imgur.com/tZGGsRj.png)



### Building the themes into CSS
We have themes that make use of vue-material, but these themes in no way shape or form interact with our website yet. To achieve this, we need some webpack magic.

We'll create a webpack configuration that will process our theme scss files and output them as css ready to be loaded, by taking advantage of the `public` folder we normaly use to provide custom `index.html` implementations, or `dist` if we're building:
```js
// theming.webpack.config.js
const glob = require('glob');
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const RemovePlugin = require('remove-files-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const name = (f) => `${f.match(/themes\/(.+)\/theme\.\w+$/)[1]}.css`;
const output = ({mode}) => mode === 'development' ? 'public' : 'dist';

const config = env => ({
  entry: glob.sync('./themes/**/theme.scss').map(f => f),
  mode: env.mode,
  output: {
    filename: 'delete.me',
    path: path.join(__dirname, output(env), 'themes')
  },
  plugins: [
    new CleanWebpackPlugin(),
    new RemovePlugin({
      after: {include: [path.join(__dirname, output(env), 'themes', 'delete.me')], trash: false}
    }),
    new OptimizeCssAssetsPlugin({
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    })
  ],
  module: {
    rules: [
      {
        test: /themes\/.+\/theme.scss$/,
        use: [
          {loader: 'file-loader', options: {name}},
          {loader: 'extract-loader'},
          {loader: 'css-loader?-url'},
          {loader: 'sass-loader'},
        ]
      }
    ]
  },

});

module.exports = config;
```
and then create two new scripts in your `package.json` and two more aliases,
```json
{
    "theme:serve": "webpack --config theming.webpack.conf.js --env.mode='development' --watch & echo 'Theme Service Started!'",
    "theme:build": "webpack --config theming.webpack.conf.js --env.mode='production'",
    "postbuild": "npm run theme:build",
    "preserve": "npm run theme:serve"
}
```
###### Couple of points:
- `theme:serve` and `theme:build` essentially call webpack with different `--env.mode` values, so we can output to the correct places.
- `preserve` and `postbuild` are used as alias so _you_ don't have to chain any commands.
- We're taking advantage of `&`, for serve, (which will execute both commands concurrently) so we can have the theme reload the files on public when we make changes to the files in `/themes/`which are then caught by vuejs and the application reloads

### Theme Service
The theme files are processed and outputed on the correct folders, we can access them via `/themes/[name].css` but we still haven't load it. for that we will need a singleton, 
```js
// theme.js
const makeAttr = (attr, value) => ({attr, value});
const loadedThemes = [];

export class Theme {

  loadTheme(name = '') {
    if (!name) return Promise.resolve(false);
    if (document.querySelector(`#vue-material-theme-${name}`)) return Promise.resolve(true);

    return new Promise(resolve => {
      const themeElement = document.createElement('link');

      themeElement.onload = () => {
        loadedThemes.push(name);
        resolve(true)
      };

      themeElement.onerror = () => {
        const ele = document.getElementById(`vue-material-theme-${name}`);
        if (ele) ele.parentNode?.removeChild(ele);
        resolve(false);
      };

      [
        makeAttr('rel', 'stylesheet'),
        makeAttr('id', `vue-material-theme-${name}`),
        makeAttr('type', 'text/css'),
        makeAttr('href', `/themes/${name}.css`),
      ].forEach(({attr, value}) => themeElement.setAttribute(attr, value));

      document.getElementsByTagName('head').item(0)?.appendChild(themeElement);
    });
  }
}

export const ThemeService = new Theme();
```

With the `ThemeService` singleton we're almost ready to make magic happen: All it's left to do is simply call `ThemeService.loadTheme('main')` when our application starts _and_ tell VueMaterial to use `main` (even if it doesn't know what main is) as a theme:

on your main file,
```js
Vue.use(VueMaterial);
Vue.material.theming.theme = 'main';
```

and in your `App.vue` file, just add a new method that waits for the resolution of `ThemeService.loadTheme()`:

```js
// App.vue
// ...
async changeTheme(name = 'main') {
    const loaded = await ThemeService.loadTheme(name);
    if (loaded) this.$material.theming.theme = name;
    // if !loaded, something happened. change Theme class at will to debug stuff
}
```

Don't forget to call this function on the `mounted()` hook as well!

## Final thoughts
###### Why are we running parallel watches and dont hook on vuejs?
VueJS isn't much permissive in its entry files, even with webpackChain we would have to accomudate for too much loaders, uses and rules. Since we never actually need the scss that vuejs parses since our scss will always live outside the src file, we can ignore it altogether. Granted, it's a bit ugly - shout me up if you know a better solution!