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/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
+
'%3E%3Cpath fill='transparent' d='M0 13h95v94H0Z' id='path850' style='fill:none'/%3E%3Cpath fill='%23eee' d='M96 107.5H0v-95h96Zm-94.197-1.795h92.393v-91.41H1.803Z' id='path852' style='fill:%23eee;fill-opacity:1'/%3E%3Cpath fill='%23fff' d='M64.294 86.574c1.903 3.108 4.379 5.392 8.759 5.392 3.679 0 6.029-1.839 6.029-4.379 0-3.044-2.414-4.123-6.464-5.894l-2.219-.952c-6.407-2.729-10.663-6.149-10.663-13.378 0-6.659 5.073-11.728 13.003-11.728 5.645 0 9.704 1.965 12.628 7.109l-6.914 4.439c-1.522-2.73-3.164-3.805-5.714-3.805-2.601 0-4.249 1.65-4.249 3.805 0 2.663 1.65 3.742 5.459 5.392l2.22.951c7.544 3.235 11.803 6.533 11.803 13.948 0 7.993-6.279 12.373-14.713 12.373-8.246 0-13.573-3.929-16.18-9.079 0-.002 7.215-4.194 7.215-4.194zm32.029 0c1.903 3.108 4.379 5.392 8.759 5.392 3.679 0 6.029-1.839 6.029-4.379 0-3.044-2.414-4.123-6.464-5.894l-2.219-.952c-6.407-2.729-10.663-6.149-10.663-13.378 0-6.659 5.073-11.728 13.003-11.728 5.645 0 9.704 1.965 12.628 7.109l-6.914 4.439c-1.522-2.73-3.164-3.805-5.714-3.805-2.601 0-4.249 1.65-4.249 3.805 0 2.663 1.65 3.742 5.459 5.392l2.22.951C115.741 76.76 120 80.058 120 87.473c0 7.993-6.279 12.373-14.713 12.373-8.246 0-13.573-3.929-16.18-9.079zm-63.393.77c1.395 2.475 2.664 4.567 5.714 4.567 2.917 0 4.757-1.141 4.757-5.579V56.141h8.878v30.31c0 9.193-5.39 13.378-13.258 13.378-7.109 0-11.226-3.679-13.32-8.11l7.229-4.375c0-.001 0 0 0 0z' id='path854'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
+
\ 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;
}
}
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];
}
});
}