From f8c4e949504b4ca56f75d90b3b7cc1ce2dfb8f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Wolak?= Date: Sun, 6 Mar 2022 12:56:04 +0100 Subject: [PATCH 1/2] feat(angular-jss): theme options `createTheme` and `createPalette` --- README.md | 41 ++++++++++++++ libs/angular-jss/README.md | 45 ++++++++++++++++ libs/angular-jss/src/index.ts | 1 + libs/angular-jss/src/lib/angular-jss.types.ts | 15 +++++- libs/angular-jss/src/lib/jss/utils/sheets.ts | 2 +- .../src/lib/styled/create-use-styles.ts | 2 +- libs/angular-jss/src/lib/styled/internals.ts | 2 +- .../src/lib/styled/styled.decorator.ts | 2 +- .../{styled.interface.ts => styled.types.ts} | 2 +- .../src/lib/theme/colors/common.ts | 6 +++ .../angular-jss/src/lib/theme/colors/index.ts | 1 + .../src/lib/theme/create-palette.ts | 21 ++++++++ .../angular-jss/src/lib/theme/create-theme.ts | 21 +++++--- libs/angular-jss/src/lib/theme/index.ts | 3 ++ .../src/lib/theme/theme-context.ts | 7 +-- .../src/lib/utils/deepmerge.test.ts | 51 ++++++++++++++++++ libs/angular-jss/src/lib/utils/deepmerge.ts | 53 ++++++++++++------- 17 files changed, 236 insertions(+), 39 deletions(-) rename libs/angular-jss/src/lib/styled/{styled.interface.ts => styled.types.ts} (100%) create mode 100644 libs/angular-jss/src/lib/theme/colors/common.ts create mode 100644 libs/angular-jss/src/lib/theme/colors/index.ts create mode 100644 libs/angular-jss/src/lib/theme/create-palette.ts create mode 100644 libs/angular-jss/src/lib/theme/index.ts create mode 100644 libs/angular-jss/src/lib/utils/deepmerge.test.ts diff --git a/README.md b/README.md index ade23fb..5ddfae4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ - [x] Component decorator `Styled` - [x] Theming with `Theme` +- [ ] Theme switching (dark/light mode) - [x] Server Side Rendering with Angular Universal +- [ ] Critical CSS ## Table of Contents @@ -107,6 +109,45 @@ export class AppComponent { ## Config options +```ts +import { create, Jss } from 'jss'; +import extend from 'jss-plugin-extend'; +import propsSort from 'jss-plugin-props-sort'; +import { JssOptions } from '@design4pro/angular-jss'; + +const jss: Jss = create({ + // additional JSS plugins @see https://cssinjs.org/plugins?v=v10.9.0 + plugins: [ + extend(), + propsSort() + ], +}); + +const jssOptions: JssOptions = { + jss: jss, + normalize: false // disable normalizing (normalize.css) +}; + +const theme: Theme = { + palette: { + primary: { + main: '#00bcd4' // use in decorator `theme.palette?.primary?.main` + }, + secondary: { + main: '#f50057' + } + } +}; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, AngularJssModule.forRoot(jssOptions, theme)], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + ## License [MIT](https://github.com/design4pro/angular-jss/blob/master/LICENSE.md) © DESIGN4 ᴾ ᴿ ᴼ diff --git a/libs/angular-jss/README.md b/libs/angular-jss/README.md index 0f6279f..5ddfae4 100644 --- a/libs/angular-jss/README.md +++ b/libs/angular-jss/README.md @@ -13,6 +13,12 @@ ## Features +- [x] Component decorator `Styled` +- [x] Theming with `Theme` +- [ ] Theme switching (dark/light mode) +- [x] Server Side Rendering with Angular Universal +- [ ] Critical CSS + ## Table of Contents - [Installation](#installation) @@ -103,6 +109,45 @@ export class AppComponent { ## Config options +```ts +import { create, Jss } from 'jss'; +import extend from 'jss-plugin-extend'; +import propsSort from 'jss-plugin-props-sort'; +import { JssOptions } from '@design4pro/angular-jss'; + +const jss: Jss = create({ + // additional JSS plugins @see https://cssinjs.org/plugins?v=v10.9.0 + plugins: [ + extend(), + propsSort() + ], +}); + +const jssOptions: JssOptions = { + jss: jss, + normalize: false // disable normalizing (normalize.css) +}; + +const theme: Theme = { + palette: { + primary: { + main: '#00bcd4' // use in decorator `theme.palette?.primary?.main` + }, + secondary: { + main: '#f50057' + } + } +}; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, AngularJssModule.forRoot(jssOptions, theme)], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + ## License [MIT](https://github.com/design4pro/angular-jss/blob/master/LICENSE.md) © DESIGN4 ᴾ ᴿ ᴼ diff --git a/libs/angular-jss/src/index.ts b/libs/angular-jss/src/index.ts index f34ea2f..dbe85d2 100644 --- a/libs/angular-jss/src/index.ts +++ b/libs/angular-jss/src/index.ts @@ -4,3 +4,4 @@ export * from './lib/angular-jss.service'; export * from './lib/angular-jss.types'; export * from './lib/ssr'; export * from './lib/styled'; +export * from './lib/theme'; diff --git a/libs/angular-jss/src/lib/angular-jss.types.ts b/libs/angular-jss/src/lib/angular-jss.types.ts index e1024ff..7ef1616 100644 --- a/libs/angular-jss/src/lib/angular-jss.types.ts +++ b/libs/angular-jss/src/lib/angular-jss.types.ts @@ -1,4 +1,5 @@ import { JssOptions } from './jss/types'; +import { ColorCommon } from './theme/colors/common'; export interface Options extends JssOptions { normalize?: boolean; @@ -7,8 +8,7 @@ export interface Options extends JssOptions { export interface Theme { breakpoints?: ThemeBreakpoints; direction?: string; - overrides?: object; - props?: object; + palette?: ThemePalette; } export interface ThemeBreakpoints { @@ -24,3 +24,14 @@ export interface ThemeBreakpoints { unit?: string; step?: number; } + +export type ThemeType = string | 'auto' | 'light' | 'dark'; + +export interface ThemePaletteCommonColor { + [key: string]: ColorCommon; +} + +export interface ThemePalette { + mode: ThemeType; + common: typeof ColorCommon; +} diff --git a/libs/angular-jss/src/lib/jss/utils/sheets.ts b/libs/angular-jss/src/lib/jss/utils/sheets.ts index d9119e2..979496d 100644 --- a/libs/angular-jss/src/lib/jss/utils/sheets.ts +++ b/libs/angular-jss/src/lib/jss/utils/sheets.ts @@ -1,6 +1,6 @@ import { getDynamicStyles, StyleSheet, StyleSheetFactoryOptions } from 'jss'; import { Theme } from '../../angular-jss.types'; -import { StyledProps } from '../../styled/styled.interface'; +import { StyledProps } from '../../styled/styled.types'; import { ThemeContext } from '../../theme/theme-context'; import { JssContext } from '../context'; import { getManager } from '../managers'; diff --git a/libs/angular-jss/src/lib/styled/create-use-styles.ts b/libs/angular-jss/src/lib/styled/create-use-styles.ts index c7be753..c20c68d 100644 --- a/libs/angular-jss/src/lib/styled/create-use-styles.ts +++ b/libs/angular-jss/src/lib/styled/create-use-styles.ts @@ -15,7 +15,7 @@ import { updateDynamicRules, } from '../jss/utils/sheets'; import { ThemeContext } from '../theme/theme-context'; -import { StyledProps } from './styled.interface'; +import { StyledProps } from './styled.types'; const createUseStyles = (doCheck: BehaviorSubject, onDestroy: Subject) => diff --git a/libs/angular-jss/src/lib/styled/internals.ts b/libs/angular-jss/src/lib/styled/internals.ts index f1b9148..cedcd23 100644 --- a/libs/angular-jss/src/lib/styled/internals.ts +++ b/libs/angular-jss/src/lib/styled/internals.ts @@ -9,7 +9,7 @@ import { DirectiveDef, DirectiveType, } from './ivy'; -import { StyledProps } from './styled.interface'; +import { StyledProps } from './styled.types'; /** * Applied to definitions and informs that class is decorated diff --git a/libs/angular-jss/src/lib/styled/styled.decorator.ts b/libs/angular-jss/src/lib/styled/styled.decorator.ts index ee389f4..993ec01 100644 --- a/libs/angular-jss/src/lib/styled/styled.decorator.ts +++ b/libs/angular-jss/src/lib/styled/styled.decorator.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import { BehaviorSubject, Subject } from 'rxjs'; import { generateStyles, markAsDecorated, STYLED_PROPS } from './internals'; import { ComponentType, DirectiveType } from './ivy'; -import { StyledProps } from './styled.interface'; +import { StyledProps } from './styled.types'; // eslint-disable-next-line @typescript-eslint/naming-convention export function StyledProp(): PropertyDecorator { diff --git a/libs/angular-jss/src/lib/styled/styled.interface.ts b/libs/angular-jss/src/lib/styled/styled.types.ts similarity index 100% rename from libs/angular-jss/src/lib/styled/styled.interface.ts rename to libs/angular-jss/src/lib/styled/styled.types.ts index e05df71..9e34e6a 100644 --- a/libs/angular-jss/src/lib/styled/styled.interface.ts +++ b/libs/angular-jss/src/lib/styled/styled.types.ts @@ -1,5 +1,5 @@ -import { HookOptions, Styles } from '../jss/types'; import { Theme } from '../angular-jss.types'; +import { HookOptions, Styles } from '../jss/types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type StyledProps = (context: StyledContext) => any; diff --git a/libs/angular-jss/src/lib/theme/colors/common.ts b/libs/angular-jss/src/lib/theme/colors/common.ts new file mode 100644 index 0000000..73a11e4 --- /dev/null +++ b/libs/angular-jss/src/lib/theme/colors/common.ts @@ -0,0 +1,6 @@ +export enum ColorCommon { + black = '#000', + white = '#fff', +}; + +export default ColorCommon; diff --git a/libs/angular-jss/src/lib/theme/colors/index.ts b/libs/angular-jss/src/lib/theme/colors/index.ts new file mode 100644 index 0000000..d0b9323 --- /dev/null +++ b/libs/angular-jss/src/lib/theme/colors/index.ts @@ -0,0 +1 @@ +export * from './common'; diff --git a/libs/angular-jss/src/lib/theme/create-palette.ts b/libs/angular-jss/src/lib/theme/create-palette.ts new file mode 100644 index 0000000..a0876ed --- /dev/null +++ b/libs/angular-jss/src/lib/theme/create-palette.ts @@ -0,0 +1,21 @@ +import { ThemePalette } from '../angular-jss.types'; +import deepmerge from '../utils/deepmerge'; +import common from './colors/common'; + +function createPalette(palette: ThemePalette): ThemePalette { + const { mode = 'light', ...other } = palette; + + const paletteOutput = deepmerge( + { + // A collection of common colors. + common: common, + // The palette mode, can be light or dark. + mode, + }, + other + ); + + return paletteOutput; +} + +export default createPalette; diff --git a/libs/angular-jss/src/lib/theme/create-theme.ts b/libs/angular-jss/src/lib/theme/create-theme.ts index 8745984..651c3c2 100644 --- a/libs/angular-jss/src/lib/theme/create-theme.ts +++ b/libs/angular-jss/src/lib/theme/create-theme.ts @@ -1,26 +1,31 @@ -import { Theme, ThemeBreakpoints } from '../angular-jss.types'; +import { Theme, ThemeBreakpoints, ThemePalette } from '../angular-jss.types'; import deepmerge from '../utils/deepmerge'; import createBreakpoints from './create-breakpoints'; +import createPalette from './create-palette'; // eslint-disable-next-line @typescript-eslint/no-explicit-any function createTheme(options: Theme = {}, ...args: any): Theme { - const { breakpoints: input = {}, ...other } = options; + const { + breakpoints: breakpointsInput = {}, + palette: paletteInput = {}, + ...other + } = options; - const _breakpoints = createBreakpoints(input as ThemeBreakpoints); + const breakpoints = createBreakpoints(breakpointsInput as ThemeBreakpoints); + const palette = createPalette(paletteInput as ThemePalette); - let theme = deepmerge( + let theme = deepmerge( { - breakpoints: _breakpoints, + breakpoints: breakpoints, direction: 'ltr', - overrides: {}, // Inject custom styles - props: {}, // Provide default props + palette: palette, }, other ); theme = args.reduce( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (acc: Theme, argument: any) => deepmerge(acc, argument), + (acc: Theme, argument: any) => deepmerge(acc, argument), theme ); diff --git a/libs/angular-jss/src/lib/theme/index.ts b/libs/angular-jss/src/lib/theme/index.ts new file mode 100644 index 0000000..c943836 --- /dev/null +++ b/libs/angular-jss/src/lib/theme/index.ts @@ -0,0 +1,3 @@ +export * from './colors'; +export * from './create-breakpoints'; +export * from './create-theme'; diff --git a/libs/angular-jss/src/lib/theme/theme-context.ts b/libs/angular-jss/src/lib/theme/theme-context.ts index 0f6f56e..a82d652 100644 --- a/libs/angular-jss/src/lib/theme/theme-context.ts +++ b/libs/angular-jss/src/lib/theme/theme-context.ts @@ -1,14 +1,11 @@ import { Injectable } from '@angular/core'; -import { ThemeBreakpoints } from '../angular-jss.types'; +import { ThemeBreakpoints, ThemePalette } from '../angular-jss.types'; import { Store } from '../utils/store'; -export type ThemeType = string | 'auto' | 'light' | 'dark'; - export class ThemeContext { breakpoints?: ThemeBreakpoints; direction?: string; - overrides?: object; - props?: object; + palette?: ThemePalette; } @Injectable() diff --git a/libs/angular-jss/src/lib/utils/deepmerge.test.ts b/libs/angular-jss/src/lib/utils/deepmerge.test.ts new file mode 100644 index 0000000..d49f934 --- /dev/null +++ b/libs/angular-jss/src/lib/utils/deepmerge.test.ts @@ -0,0 +1,51 @@ +import deepmerge from './deepmerge'; + +describe('deepmerge', () => { + it('should not be subject to prototype pollution', () => { + deepmerge( + {}, + JSON.parse('{ "myProperty": "a", "__proto__" : { "isAdmin" : true } }'), + { + clone: false, + } + ); + + expect({}).not.toHaveProperty('isAdmin'); + }); + + it('should not merge HTML elements', () => { + const element = document.createElement('div'); + const element2 = document.createElement('div'); + + const result = deepmerge({ element }, { element: element2 }); + + expect(result.element).toEqual(element2); + }); + + it('should reset source when target is undefined', () => { + const result = deepmerge( + { + '&.disabled': { + color: 'red', + }, + }, + { + '&.disabled': undefined, + } + ); + expect(result).toEqual({ + '&.disabled': undefined, + }); + }); + + it('should merge keys that do not exist in source', () => { + const result = deepmerge( + { foo: { baz: 'test' } }, + { foo: { bar: 'test' }, bar: 'test' } + ); + expect(result).toEqual({ + foo: { baz: 'test', bar: 'test' }, + bar: 'test', + }); + }); +}); diff --git a/libs/angular-jss/src/lib/utils/deepmerge.ts b/libs/angular-jss/src/lib/utils/deepmerge.ts index b256de4..b50ae67 100644 --- a/libs/angular-jss/src/lib/utils/deepmerge.ts +++ b/libs/angular-jss/src/lib/utils/deepmerge.ts @@ -1,35 +1,50 @@ -import { Theme } from '../angular-jss.types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +function isObject(o: T) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + +export function isPlainObject(o: T) { + if (isObject(o) === false) return false; + + // If has modified constructor + const ctor = (o).constructor; + if (ctor === undefined) return true; + + // If has modified prototype + const prot = ctor.prototype; + if (isObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) { + return false; + } -export function isPlainObject(item: Theme) { - return item && typeof item === 'object' && item.constructor === Object; + // Most likely a plain Object + return true; } -export default function deepmerge( - target: Theme, - source: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - direction?: string | undefined; - overrides?: object | undefined; - props?: object | undefined; - }, +export default function deepmerge( + target: T, + source: S, options = { clone: true } -): Theme { - // eslint-disable-next-line @typescript-eslint/no-explicit-any +): T { const output: any = options.clone ? { ...target } : target; - if (isPlainObject(target) && isPlainObject(source)) { + if (isPlainObject(target) && isPlainObject(source)) { Object.keys(source).forEach((key) => { // Avoid prototype pollution if (key === '__proto__') { return; } - if (isPlainObject(source[key]) && key in target) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - output[key] = deepmerge((target)[key], source[key], options); + if (isPlainObject((source)[key]) && key in target) { + output[key] = deepmerge( + (target)[key], + (source)[key], + options + ); } else { - output[key] = source[key]; + output[key] = (source)[key]; } }); } From 5f4c0cd6fb6524acda5f5d04175670164e9bbf10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Wolak?= Date: Sun, 6 Mar 2022 12:56:36 +0100 Subject: [PATCH 2/2] docs(website): update website demo --- apps/webpage/src/app/app.component.css | 0 apps/webpage/src/app/app.component.html | 15 ++++++++++++--- apps/webpage/src/app/app.component.ts | 18 +++++++++++------- 3 files changed, 23 insertions(+), 10 deletions(-) delete mode 100644 apps/webpage/src/app/app.component.css diff --git a/apps/webpage/src/app/app.component.css b/apps/webpage/src/app/app.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/apps/webpage/src/app/app.component.html b/apps/webpage/src/app/app.component.html index bb11e48..b9f680c 100644 --- a/apps/webpage/src/app/app.component.html +++ b/apps/webpage/src/app/app.component.html @@ -1,3 +1,12 @@ -
- - \ No newline at end of file + +
+

+ Welcome to {{ title }}! +

+

Hover the logo

+ Angular Logo +
\ No newline at end of file diff --git a/apps/webpage/src/app/app.component.ts b/apps/webpage/src/app/app.component.ts index b27fefe..3b09c92 100644 --- a/apps/webpage/src/app/app.component.ts +++ b/apps/webpage/src/app/app.component.ts @@ -1,10 +1,10 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Styled, StyledProp, Theme } from '@design4pro/angular-jss'; @Component({ selector: 'angular-jss-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, }) @Styled(({ css, injectGlobal }) => { injectGlobal({ @@ -18,25 +18,29 @@ import { Styled, StyledProp, Theme } from '@design4pro/angular-jss'; return css( (theme: Theme) => ({ root: { - color: '#fff', + textAlign: 'center', + }, + title: { + color: theme.palette?.common?.white, backgroundColor: 'var(--background-color)', padding: '20px', direction: theme.direction, }, + hint: { + color: theme.palette?.common?.black, + }, }), { name: 'first' } ); }) export class AppComponent { - title = 'angular-jss'; + title = 'Angular JSS'; classes: any; - name?: string; @StyledProp() color = 'red'; - click() { + onMouseEvent() { this.color = this.color === 'red' ? 'green' : 'red'; - this.name = this.color; } }