Multi-themed Web Project: Front-end
Following the previous posts that addressed some of the issues faced trying to create a multi-themed project in an Angular and ASP.NET Core project, I would now like to start discussing some of the decisions and their implementations that I made regarding the front-end portion of this application.
The main obstacle faced to date was the need to be able to create the multiple themes through one build and then reference those built files correctly per organization. Referencing those files correctly per organization has already been discussed in the post addressing the back end concerns. It is being handled through an MVC Controller/View combination which is dynamically routing the user to the appropriate 'index' view based on the URL they are visiting the site through.
This article will focus on how we generate/maintain the theme files that the MVC application is then able to reference. In order to create the per organization theme files, we took a two-pronged approach. For the bulk of the styles, we punted and chose a very popular, flexible, front-end framework called Semantic UI. For various tweaks or items we felt were 'missing' from the framework, we created a few scss files and a build process around them.
While researching our options in this area, we investigated:
1. Creating all of our own styles from scratch
- This option seemed a bit too daunting as the team allocated for this project was only two developers, neither of which had previously focused on or specialized in CSS
2. Utilizing Bootstrap
- This option seemed very attractive as it seems to be a highly utilized/battle-tested framework. BUT, we avoided this for two reasons...
1. Bootstrap v3 is the stable platform to date, but looking into creating different themes within their framework seemed difficult at best.
2. Bootstrap v4 seems to be much more malleable in this regard as it is built on SCSS, but it is in alpha and has been for a long while.
3. Utilizing Semantic UI
- This framework provides a nice way to create themes and a number of hooks to enable further customization, as needed.
- It is written with LESS and built with gulp which provided us a couple of hooks to enable the types of customizations we were aiming for with this project.
I opted for a mix of options 1 & 3. The last post discussed how I managed to wrangle Semantic UI's build process into producing the css output for multiple themes with one build. As mentioned also in previous posts, while Semantic UI utilizes gulp for its build process, the custom build tasks I wanted to create were relatively small and focused so I decided to rely on executing my custom build tasks through npm scripts.
NPM Scripts can execute node commands. There is a nice npm package called ts-node
that allows the developer to write their node scripts in Typescript and execute them as node tasks. This runs the compilation for the Typescript files and then executes the transpiled JavaScript. I enjoy using Typescript whenever I can in place of JavaScript both for the type safety it provides as well as the explorability gains that it gives through its type system.
The code I reference for this post can be found on GitHub.
As a bare minimum for what I was hoping to achieve, I defined these npm scripts:
"postinstall": "ncp build/override-semantic-ui-build.js src/styles/semantic/tasks/build.js",
"prebuild": "npm run lint",
"build": "ts-node build/buildScss & gulp --gulpfile ./styles/semantic/gulpfile.js build",
"lint": "npm run lint:ts & npm run lint:sass",
"lint:ts": "tslint --type-check --project tsconfig.json --config config/tslint.json build/**/*.ts",
"lint:sass": "sass-lint -c config/sasslint.yml -v"
As mentioned in a previous post on setting up a Semantic UI build that creates artifacts for multiple themes, the postinstall
script overwrites the single build file that needs to be updated to support that functionality. The prebuild
task runs all of the linting tasks and is run, by convention, automatically before the build task is executed. These utilize standard linting tools that are well documented sass-lint, tslint.
The only other task in that list is the build
task. The second command in the build script is the one that executes the Semantic UI gulp build task. At a high-level, the basic directory structure for the front end of the application relating specifically to the multi-themed styling is as follows:
/build
/src
/styles
/semantic
/src
/themes
/{registered_organization_abbreviation}
/globals
site.variables
theme.config
/site
/shared
/framework
_colors.scss
_site.scss
{registered_organization_abbreviation}.scss
package.json
The build
directory is where I chose to place all of the scripts I've created for the build process. The styles
directory as shown is broken into two main sections. The semantic
directory contains the semantic specific styles and the location of the theme specific files which follow semantic's naming/placement conventions. The site
directory contains all the additional styles that semantic doesn't provide that are specific to the site under construction.
Returning to the build task at hand, the first command that the build
script executes is contained in the build/buildScss.ts
file.
import { writeFile } from 'fs';
import * as mkdirp from 'mkdirp';
import { render, Result, SassError } from 'node-sass';
import { argv } from 'process';
import { handleError } from './handleError';
/* tslint:disable-next-line */
const value: IThemeJson = require('./themes.json');
const isProductionBuild: boolean = !!argv.find((arg: string) => arg === 'production' || arg === 'prod' || arg === '-P' || arg === '--prod');
value.themes.forEach((theme: string) => {
const outputDir: string = `./styles/dist/${theme}`;
// TODO: Add File Hashing
mkdirp(outputDir, (error: Error): void => {
handleError(error);
const inputDirRoot: string = `./styles`;
const inputFilePath: string = `${inputDirRoot}/${theme}.scss`;
const cssOutputFilePath: string = `${outputDir}/${theme}.min.css`;
const cssMapOutputFilePath: string = `${outputDir}/${theme}.css.map`;
render({
file: inputFilePath,
outFile: cssOutputFilePath,
outputStyle: isProductionBuild ? 'compressed' : 'expanded',
sourceMap: !isProductionBuild
},
(sassError: SassError, result: Result): void => {
handleError(sassError);
writeFile(cssOutputFilePath, result.css, handleError);
if (!isProductionBuild) {
writeFile(cssMapOutputFilePath, result.map, handleError);
}
});
});
});
As can be seen, it utilizes an npm package called node-sass
to process all of the scss files and create the transpiled .css
file. As seen in many of the other scripts, it first retrieves the list of licensed organizationa contained in the relevant .json
file, loops through each theme, creates an output directory for each organization and then places the transpiled .css
files in that directory. The goal is to either pull as much of the styling for the site from semantic's styles or to put as much of the sites styling in the framework
directory shown above. Each licensed organization's specific .scss
file will then look something like this:
@import './shared/framework/colors';
$primary-color: blue;
$secondary-color: red;
@import './shared/site';
What this means is that the transpilation done by node-sass
will utilize the framework files as the defaults(currently just organizational specific colors, but could be extended to include fonts, images, etc). Then we provide the organization's specific overrides for any/all of those values and after that include the shared site styles. In this manner whatever overrides have been provided will be utilized as the values for the site's variable replacements. This also provides the flexibility to add overrides to the site's styles, if need be, by placing those specific styles after the @import './shared/site';
line. Obviously, the more specification that is done at this level abrogates the usefulness of this approach as you end up building more and more of the css for each site specifically, but the power is in your hands to do as you please. The task then outputs the .css
files in their respective licensed organization's directory and violĂ , you now have a project that has multiple css themes built in one command. As this post is a bit longer than I was expecting it to be, I'll follow up with one last post on this subject relating to a few other npm scripts I created to help with setting up new licensed organizations as well as getting these generated css files to play nicely with ASP.NET core and Angular.