Why? Well, "Coming soon..." 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 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
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
@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
andtheme.scss
Populate variables.scss
with
$theme-name: 'main' !default;
$primary-color: pink !default;
$secondary-color: blue !default;
$danger-color: red !default;
and theme.scss
with
@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
$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
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:
// 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,
{
"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
andtheme:build
essentially call webpack with different--env.mode
values, so we can output to the correct places. -
preserve
andpostbuild
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,
// 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,
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()
:
// 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!
Top comments (0)