CSS (a.k.a. Cascading Style Sheets) was a huge step forward in the evolution of the Web; it allowed Web designers to apply a variety of themes to a site just by referencing a different CSS file. Later on, CSS pre-processors like Less.js extended the CSS language by adding features that allow variables, mixins, functions, and many other techniques to make your CSS easier to maintain. Less’s most useful feature is arguably its support for variables. These save you from having to write out the same color every time that you want to apply it to a page element. But it doesn’t end there; variables also can be employed to set dynamic paths to client subfolders. By combining variables with Less’s powerful @import statement, you can generate CSS files for all your themes at once. In today’s article, we’ll explore how it’s done.
The Project Structure
Here’s a screenshot of a typical Angular project’s CSS file structure:
Figure 1: The file structure
As you can see, there are some default files and several theme directories. Each of these contains a variables.less file that defines the color scheme for that theme.
Importing Other .less Files
The default.less file in the root css folder imports all of the required Less files, including default-common.less, the variables.less file, as well as the one for the theme. It includes the @{ClientDir} variables so that we can substitute the various theme paths:
@import './default-common.less'; @import './variables.less'; @import '@{ClientDir}/variables.less'; .neutral-background { background: @Neutral1Color; } .neutral-border { border-color: @Neutral1Color; }
Generating the CSS
Now, let’s take a look at the JavaScript that iterates over all of the folders and files in the css directory and generates the CSS using the less.render() method:
#! /usr/bin/env node "use strict"; var args = process.argv.slice(2), outputDir = args.length == 1 ? args[0] + 'assets/css/'; // Use default path if there is no argument : './src/app/shared/assets/css/', fs = require('fs-extra'), less = require('less'), path = require('path'), filePath = './src/app/shared/assets/css/', DEFAULT = 'default.less', defaultLess = path.join(filePath, DEFAULT)); (function () { fs.readdir(filePath, null, function (err, items) { if (err) { console.error(err); return; } for (var i = 0; i < items.length; i++) { var themeSrcPath = path.join(filePath, items[i]); // Is this a theme folder? if (fs.lstatSync(themeSrcPath).isDirectory()) { var theme = items[i]; console.log('Compiling CSS for ${theme}'); processFile(defaultLess, theme); } else if (items[i] === DEFAULT) { processFile(themeSrcPath); } } }); } function processFile(lessFile, themeDir) { var input = fs.readFileSync(lessFile, 'utf8'), options = { modifyVars: { 'themeDir': themeDir ? themeDir.trim() : '.' }, filename: path.resolve( lessFile ) }; less.render(input, options, function(e, result) { if (e) { console.log(e); } else { var filename = path.basename(lessFile, '.less'), outputPath = themeDir ? path.join(outputDir, themeDir, filename + '.css') : path.join(outputDir, filename + '.css'); console.log('Compiling ${lessFile} for ${themeDir}...'); fs.writeFileSync(outputPath, result.css, 'utf8'); } }); })();
In the preceding processFile() function, the theme folder is inserted into the path by setting the options.modifyVars attribute, along with the filename option.
Conclusion
Combining Less variables with the @import statement is a great way to generate CSS files for all your themes at once. In Angular, you can even hook this process into your package.json file so that you can make it part of your project builds.