Commit 31f050be authored by Shane Eckenrode's avatar Shane Eckenrode

Merge branch 'feature/theming-utils' into 'develop'

Feature/theming utils

See merge request !6
parents 6873af4c 3da56e3e
Pipeline #85117 passed with stages
in 4 minutes and 25 seconds
......@@ -287,6 +287,34 @@
"styleext": "scss"
}
}
},
"utils-theming": {
"projectType": "library",
"root": "libs/utils/theming",
"sourceRoot": "libs/utils/theming/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/theming/tsconfig.lib.json", "libs/utils/theming/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/theming/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/theming/jest.config.js",
"tsConfig": "libs/utils/theming/tsconfig.spec.json",
"setupFile": "libs/utils/theming/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
}
},
"cli": {
......
# utils-theming
# Setup for Alternate Theming
## Prerequisites
- Install @psu/components, @psu/utils, @psu/palette
- Install node-sass and node-sass-tilde-importer as Dev Dependencies
- Application must be setup to use scss and recommended to adhere to the following scss styling structure
- If not already existing, you must have the following files within the src directory at the **_root of the application_**
- `_app-theme.scss`: Import all application, internal application libraries, or external libraries that have theming here and include them within a mixin named `@mixin app-theme($theme)`.
- `_theme.scss`: Set up the primary application theme variables here. This file should import the theme palette from `@psu/palette` and set up the base theme colors. It should follow the structure from the snippet below
```scss
@import '~@psu/palette/beaver-blue';
// Setting Theme Colors
$primaryPalette: mat-palette($primary, 700, 50, 900);
$secondaryPalette: mat-palette($secondary);
$errorPalette: mat-palette($error);
$appBackground: #f5f5f5;
```
- `_typography.scss`: Set up the application typography variables here. Follow the structure from the snippet below. More information can be found in the [typography section](https://git.psu.edu/ais-swe/ux/theme-lib#typography) of the theme utils readme.
```scss
// Setting Typography for all themes
$titleFont: 'Roboto Slab';
$bodyFont: 'Open Sans';
$titleRegularWeight: 400;
$titleBoldWeight: 700;
$bodyRegularWeight: 400;
$bodyBoldWeight: 600;
```
- Import each of the above files into `styles.scss` and follow instructions for [theme generation](https://git.psu.edu/ais-swe/ux/theme-lib#theming) and [typography generation](https://git.psu.edu/ais-swe/ux/theme-lib#typography) from theme utils readme.
## Application-Specific Setup
1. Copy the following snippet into a new file named `build-themes.sh` and place in the `tools` directory. The tools directory should be located at the root of the project.
```sh
#!/bin/bash
DEST_PATH=apps/ui/src/assets
INPUT_PATH=$DEST_PATH/alternate-themes/
echo Building custom theme scss files.
# Get the files
FILES=$(find apps/ui/src/assets/alternate-themes -name "*.scss")
for FILE in $FILES
do
FILENAME=${FILE#$INPUT_PATH}
BASENAME=${FILENAME%.scss}
$(npm bin)/node-sass --importer=node_modules/node-sass-tilde-importer $FILE > $DEST_PATH/$BASENAME.css
done
echo Finished building CSS.
```
2. Create the `alternate-themes` directory at the path `apps/ui/src/assets`. This is where you will add your alternate theme scss files. These scss files should follow the following structure:
```scss
@import '~@psu/theme-lib/md2/md2'; // Import the theming utilities
@import '~@psu/palette/nittany-navy'; // Import the alternate theme palette
@import '../../app-theme'; // Import the app-theme mixin to propagate the alternate theme to all angular material, application, and library components
// Setting Theme Colors
$primaryPalette: mat-palette($primary, 700, 50, 900);
$secondaryPalette: mat-palette($secondary);
$errorPalette: mat-palette($error);
$appBackground: #f5f5f5;
// Generate the new alternate theme and propagate it to all components
$theme: generate-material-light-theme(
$primaryPalette,
$secondaryPalette,
$errorPalette,
$status-colors,
$appBackground-light
);
@include angular-material-theme($theme);
@include app-theme($theme);
```
3. In the `angular.json`, add the following code to the assets array of the application's build object. Path should be close to: `projects`
```json
{
"glob": "*.css",
"input": "apps/ui/src/assets",
"output": "./alt-themes/"
}
```
4. Add the `build-themes` script to the package.json and update existing scripts to call that script first.
5. Add the `STYLE_MANAGER_CONFIG` to `app.module.ts`'s providers array. Ex:
```ts
{
provide: STYLE_MANAGER_CONFIG,
useValue: [
{
name: 'beaver-blue',
primary: '#316cae',
isDefault: true
},
{
name: 'nittany-navy',
primary: '#143372'
},
{
name: 'dark',
primary: '#0d0d0d',
isDark: true
}
]
}
```
module.exports = {
name: 'utils-theming',
preset: '../../../jest.config.js',
coverageDirectory: '../../../coverage/libs/utils/theming',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};
{
"lib": {
"entryFile": "src/index.ts"
}
}
export { StyleManager, STYLE_MANAGER_CONFIG, Theme } from './lib/style-manager';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { StyleManager, STYLE_MANAGER_CONFIG } from './style-manager';
describe('StyleManager', () => {
let styleManager: StyleManager;
beforeEach(() =>
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
StyleManager,
{
provide: STYLE_MANAGER_CONFIG,
useValue: [
{ name: 'default', primary: '#ffffff', isDark: false, isDefault: true },
{ name: 'test', primary: '#123456', isDark: false, isDefault: false },
{ name: 'new', primary: '#000055', isDark: true, isDefault: false }
]
}
]
})
);
beforeEach(inject([StyleManager], (sm: StyleManager) => {
styleManager = sm;
}));
afterEach(() => {
const links = document.head.querySelectorAll('link');
// tslint:disable-next-line: prefer-const
for (let link of Array.prototype.slice.call(links)) {
if (link.className.includes('style-manager-')) {
document.head.removeChild(link);
}
}
});
it('should add stylesheet to head', () => {
styleManager.setCurrentTheme('test');
const styleEl = document.head.querySelector('.style-manager-theme') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
});
it('should change existing stylesheet', () => {
styleManager.setCurrentTheme('test');
const styleEl = document.head.querySelector('.style-manager-theme') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
styleManager.setCurrentTheme('new');
const styleEl2 = document.head.querySelector('.style-manager-theme') as HTMLLinkElement;
expect(styleEl2.href.endsWith('new.css')).toBe(true);
});
it('should remove alternate stylesheet', () => {
styleManager.setCurrentTheme('test');
const styleEl = document.head.querySelector('.style-manager-theme') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
styleManager.setCurrentTheme('default');
const styleEl2 = document.head.querySelector('.style-manager-theme') as HTMLLinkElement;
expect(styleEl2).toBeNull();
});
});
import { Inject, Injectable, InjectionToken } from '@angular/core';
export interface Theme {
name: string;
primary: string;
isDefault?: boolean;
isDark?: boolean;
}
export const STYLE_MANAGER_CONFIG = new InjectionToken<Theme[]>('style.manager.config');
/**
* Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be
* removed or changed later.
*/
@Injectable({ providedIn: 'root' })
export class StyleManager {
private _currentTheme: Theme;
private _availableThemes: Theme[];
constructor(@Inject(STYLE_MANAGER_CONFIG) private config: Theme[]) {
this.availableThemes = config;
this.currentTheme = this.availableThemes.find(theme => theme.isDefault);
}
public set currentTheme(theme: Theme) {
this._currentTheme = theme;
if (theme.isDefault) {
this.removeStyle('theme');
} else {
// This may need adjusted to allow the path to be configurable later.
this.setStyle('theme', `alt-themes/${theme.name}.css`);
}
}
public get currentTheme(): Theme {
return this._currentTheme;
}
public set availableThemes(themes: Theme[]) {
this._availableThemes = themes;
}
public get availableThemes(): Theme[] {
return this._availableThemes;
}
public setCurrentTheme(themeName: string): void {
const newTheme = this.availableThemes.find(theme => theme.name === themeName);
if (!newTheme) {
console.warn(`Couldn't find a theme with name ${themeName}.`);
} else {
this.currentTheme = newTheme;
}
}
/**
* Set the stylesheet with the specified key.
*/
private setStyle(key: string, href: string): void {
getLinkElementForKey(key).setAttribute('href', href);
}
/**
* Remove the stylesheet with the specified key.
*/
private removeStyle(key: string): void {
const existingLinkElement = getExistingLinkElementByKey(key);
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
}
function getLinkElementForKey(key: string): Element {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string): Element {
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
}
function createLinkElementWithKey(key: string): Element {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
}
function getClassNameForKey(key: string): string {
return `style-manager-${key}`;
}
import 'jest-preset-angular';
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": ["**/*.ts"]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts"]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
}
{
"extends": "../../../tslint.json",
"rules": {
"directive-selector": [true, "attribute", "ut", "camelCase"],
"component-selector": [true, "element", "ut", "kebab-case"]
}
}
......@@ -31,6 +31,9 @@
},
"utils-responsive": {
"tags": []
},
"utils-theming": {
"tags": []
}
}
}
......@@ -21,7 +21,8 @@
"@psu/utils/rx": ["libs/utils/rx/src/index.ts"],
"@psu/utils/browser": ["libs/utils/browser/src/index.ts"],
"@psu/utils/angular": ["libs/utils/angular/src/index.ts"],
"@psu/utils/responsive": ["libs/utils/responsive/src/index.ts"]
"@psu/utils/responsive": ["libs/utils/responsive/src/index.ts"],
"@psu/utils/theming": ["libs/utils/theming/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment