diff --git a/aio/angular.json b/aio/angular.json index 446de02f48..ed6581ca49 100644 --- a/aio/angular.json +++ b/aio/angular.json @@ -41,7 +41,17 @@ "src/google385281288605d160.html" ], "styles": [ - "src/styles/main.scss" + "src/styles/main.scss", + { + "inject": false, + "input": "src/styles/custom-themes/light.scss", + "bundleName": "assets/light" + }, + { + "inject": false, + "input": "src/styles/custom-themes/dark.scss", + "bundleName": "assets/dark" + }, ], "scripts": [], "budgets": [ diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index 103382ec32..a443a57f48 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -1,5 +1,4 @@
-
@@ -24,6 +23,7 @@ +
diff --git a/aio/src/app/shared/shared.module.ts b/aio/src/app/shared/shared.module.ts index afefd050e8..8c03e90299 100644 --- a/aio/src/app/shared/shared.module.ts +++ b/aio/src/app/shared/shared.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SearchResultsComponent } from './search-results/search-results.component'; import { SelectComponent } from './select/select.component'; +import { ThemePickerComponent } from './theme-picker/theme-picker.component'; @NgModule({ imports: [ @@ -9,11 +10,13 @@ import { SelectComponent } from './select/select.component'; ], exports: [ SearchResultsComponent, - SelectComponent + SelectComponent, + ThemePickerComponent ], declarations: [ SearchResultsComponent, - SelectComponent + SelectComponent, + ThemePickerComponent ] }) export class SharedModule {} diff --git a/aio/src/app/shared/theme-picker/style-manager/index.ts b/aio/src/app/shared/theme-picker/style-manager/index.ts new file mode 100644 index 0000000000..05ad917e68 --- /dev/null +++ b/aio/src/app/shared/theme-picker/style-manager/index.ts @@ -0,0 +1,3 @@ +// Taken from Angular Material docs repo + +export * from './style-manager'; diff --git a/aio/src/app/shared/theme-picker/style-manager/style-manager.spec.ts b/aio/src/app/shared/theme-picker/style-manager/style-manager.spec.ts new file mode 100644 index 0000000000..d1101ff8e7 --- /dev/null +++ b/aio/src/app/shared/theme-picker/style-manager/style-manager.spec.ts @@ -0,0 +1,56 @@ +// Taken from Angular Material docs repo + +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {inject, TestBed} from '@angular/core/testing'; +import {StyleManager} from './style-manager'; + + +describe('StyleManager', () => { + let styleManager: StyleManager; + + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [StyleManager] + })); + + beforeEach(inject([StyleManager], (sm: StyleManager) => { + styleManager = sm; + })); + + afterEach(() => { + const links = document.head.querySelectorAll('link'); + for (const link of Array.prototype.slice.call(links)) { + if (link.className.includes('style-manager-')) { + document.head.removeChild(link); + } + } + }); + + it('should add stylesheet to head', () => { + styleManager.setStyle('test', 'test.css'); + const styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement; + expect(styleEl).not.toBeNull(); + expect(styleEl.href.endsWith('test.css')).toBe(true); + }); + + it('should change existing stylesheet', () => { + styleManager.setStyle('test', 'test.css'); + const styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement; + expect(styleEl).not.toBeNull(); + expect(styleEl.href.endsWith('test.css')).toBe(true); + + styleManager.setStyle('test', 'new.css'); + expect(styleEl.href.endsWith('new.css')).toBe(true); + }); + + it('should remove existing stylesheet', () => { + styleManager.setStyle('test', 'test.css'); + let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement; + expect(styleEl).not.toBeNull(); + expect(styleEl.href.endsWith('test.css')).toBe(true); + + styleManager.removeStyle('test'); + styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement; + expect(styleEl).toBeNull(); + }); +}); diff --git a/aio/src/app/shared/theme-picker/style-manager/style-manager.ts b/aio/src/app/shared/theme-picker/style-manager/style-manager.ts new file mode 100644 index 0000000000..01d2f804b7 --- /dev/null +++ b/aio/src/app/shared/theme-picker/style-manager/style-manager.ts @@ -0,0 +1,48 @@ +// Taken from Angular Material docs repo + +import {Injectable} from '@angular/core'; + + +/** + * Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be + * removed or changed later. + */ +@Injectable() +export class StyleManager { + /** + * Set the stylesheet with the specified key. + */ + setStyle(key: string, href: string) { + getLinkElementForKey(key).setAttribute('href', href); + } + + /** + * Remove the stylesheet with the specified key. + */ + removeStyle(key: string) { + const existingLinkElement = getExistingLinkElementByKey(key); + if (existingLinkElement) { + document.head.removeChild(existingLinkElement); + } + } +} + +function getLinkElementForKey(key: string) { + return getExistingLinkElementByKey(key) || createLinkElementWithKey(key); +} + +function getExistingLinkElementByKey(key: string) { + return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`); +} + +function createLinkElementWithKey(key: string) { + const linkEl = document.createElement('link'); + linkEl.setAttribute('rel', 'stylesheet'); + linkEl.classList.add(getClassNameForKey(key)); + document.head.appendChild(linkEl); + return linkEl; +} + +function getClassNameForKey(key: string) { + return `style-manager-${key}`; +} diff --git a/aio/src/app/shared/theme-picker/theme-picker.component.html b/aio/src/app/shared/theme-picker/theme-picker.component.html new file mode 100644 index 0000000000..d5927664dd --- /dev/null +++ b/aio/src/app/shared/theme-picker/theme-picker.component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/aio/src/app/shared/theme-picker/theme-picker.component.spec.ts b/aio/src/app/shared/theme-picker/theme-picker.component.spec.ts new file mode 100644 index 0000000000..311ae9a933 --- /dev/null +++ b/aio/src/app/shared/theme-picker/theme-picker.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ThemePickerComponent } from './theme-picker.component'; + +describe('ThemePickerComponent', () => { + let component: ThemePickerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ThemePickerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ThemePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/src/app/shared/theme-picker/theme-picker.component.ts b/aio/src/app/shared/theme-picker/theme-picker.component.ts new file mode 100644 index 0000000000..666f9deb17 --- /dev/null +++ b/aio/src/app/shared/theme-picker/theme-picker.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { StyleManager } from './style-manager'; +import { ThemeStorage } from './theme-storage/theme-storage'; + +@Component({ + selector: 'aio-theme-picker', + templateUrl: './theme-picker.component.html', + providers: [ThemeStorage, StyleManager] +}) +export class ThemePickerComponent implements OnInit { + + currentTheme: 'light' | 'dark' = 'light'; + + constructor(private _themeStorage: ThemeStorage, private _styleManager: StyleManager) { } + + ngOnInit(): void { + const theme = this._themeStorage.getStoredThemeName(); + if (theme) { + this.selectTheme(theme); + } + } + + selectTheme(theme: string) { + this.currentTheme = theme as 'light' | 'dark'; + + if (theme === 'light') { + this._styleManager.removeStyle('theme'); + } else { + this._styleManager.setStyle('theme', `assets/${theme}.css`); + } + + if (this.currentTheme) { + this._themeStorage.storeTheme(this.currentTheme); + } + } +} diff --git a/aio/src/app/shared/theme-picker/theme-storage/index.ts b/aio/src/app/shared/theme-picker/theme-storage/index.ts new file mode 100644 index 0000000000..0d529e25c9 --- /dev/null +++ b/aio/src/app/shared/theme-picker/theme-storage/index.ts @@ -0,0 +1,3 @@ +// Taken from Angular Material docs repo + +export * from './theme-storage'; diff --git a/aio/src/app/shared/theme-picker/theme-storage/theme-storage.ts b/aio/src/app/shared/theme-picker/theme-storage/theme-storage.ts new file mode 100644 index 0000000000..91e70168bb --- /dev/null +++ b/aio/src/app/shared/theme-picker/theme-storage/theme-storage.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class ThemeStorage { + static storageKey = 'io-theme-storage-current-name'; + + + storeTheme(theme: 'light' | 'dark') { + try { + window.localStorage[ThemeStorage.storageKey] = theme; + } catch { } + + } + + getStoredThemeName(): 'light' | 'dark' | null { + try { + return window.localStorage[ThemeStorage.storageKey] || null; + } catch { + return null; + } + } + + clearStorage(): void { + try { + window.localStorage.removeItem(ThemeStorage.storageKey); + } catch { } + } +} diff --git a/aio/src/styles/1-layouts/_layouts-dir.scss b/aio/src/styles/1-layouts/_layouts-dir.scss index 4fec149571..0ea1d27db2 100644 --- a/aio/src/styles/1-layouts/_layouts-dir.scss +++ b/aio/src/styles/1-layouts/_layouts-dir.scss @@ -6,7 +6,7 @@ @import 'doc-viewer'; @import 'footer'; @import 'layout-global'; -@import 'marketing-layout'; +@import 'marketing-layout/marketing-layout'; @import 'not-found'; @import 'sidenav'; @import 'top-menu'; diff --git a/aio/src/styles/1-layouts/marketing-layout/_marketing-layout-theme.scss b/aio/src/styles/1-layouts/marketing-layout/_marketing-layout-theme.scss new file mode 100644 index 0000000000..d03b375167 --- /dev/null +++ b/aio/src/styles/1-layouts/marketing-layout/_marketing-layout-theme.scss @@ -0,0 +1,13 @@ +@mixin marketing-layout-theme($theme) { + $primary: map-get($theme, primary); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + $is-dark-theme: map-get($theme, is-dark); + + .background-sky { + background-color: mat-color($primary, default); + background: linear-gradient(145deg, mat-color($primary, 900), mat-color($primary, 400)); + color: mat-color($foreground, text); + } +} + \ No newline at end of file diff --git a/aio/src/styles/1-layouts/_marketing-layout.scss b/aio/src/styles/1-layouts/marketing-layout/_marketing-layout.scss similarity index 98% rename from aio/src/styles/1-layouts/_marketing-layout.scss rename to aio/src/styles/1-layouts/marketing-layout/_marketing-layout.scss index e1d99ef5fe..0c05e49f18 100644 --- a/aio/src/styles/1-layouts/_marketing-layout.scss +++ b/aio/src/styles/1-layouts/marketing-layout/_marketing-layout.scss @@ -183,11 +183,6 @@ section#intro { } // ANGULAR LINE -.background-sky { - background-color: $blue; - background: $bluegradient; - color: $white; -} .home-row .card { @include card(70%, auto); diff --git a/aio/src/styles/_app-theme.scss b/aio/src/styles/_app-theme.scss new file mode 100644 index 0000000000..db6bfcc331 --- /dev/null +++ b/aio/src/styles/_app-theme.scss @@ -0,0 +1,7 @@ +@import '~@angular/material/theming'; + +@import '1-layouts/marketing-layout/marketing-layout-theme'; + +@mixin app-theme($theme) { + @include marketing-layout-theme($theme); +} \ No newline at end of file diff --git a/aio/src/styles/custom-themes/dark.scss b/aio/src/styles/custom-themes/dark.scss new file mode 100644 index 0000000000..cbe2fc8d96 --- /dev/null +++ b/aio/src/styles/custom-themes/dark.scss @@ -0,0 +1,10 @@ +@import '../app-theme'; + + +$ng-io-primary: mat-palette($mat-red, 700, 600, 800); +$ng-io-accent: mat-palette($mat-blue, 700, 600, 800); +$ng-io-warn: mat-palette($mat-blue); +$ng-io-theme: mat-dark-theme($ng-io-primary, $ng-io-accent, $ng-io-warn); + +@include angular-material-theme($ng-io-theme); +@include app-theme($ng-io-theme); \ No newline at end of file diff --git a/aio/src/styles/custom-themes/light.scss b/aio/src/styles/custom-themes/light.scss new file mode 100644 index 0000000000..4f1eeaaac6 --- /dev/null +++ b/aio/src/styles/custom-themes/light.scss @@ -0,0 +1,10 @@ +@import '../app-theme'; + + +$ng-io-primary: mat-palette($mat-blue, 700, 600, 800); +$ng-io-accent: mat-palette($mat-red, 700, 600, 800); +$ng-io-warn: mat-palette($mat-red); +$ng-io-theme: mat-light-theme($ng-io-primary, $ng-io-accent, $ng-io-warn); + +@include angular-material-theme($ng-io-theme); +@include app-theme($ng-io-theme); \ No newline at end of file diff --git a/aio/src/styles/ng-io-theme.scss b/aio/src/styles/ng-io-theme.scss index 1b1cca8604..45f5071d27 100644 --- a/aio/src/styles/ng-io-theme.scss +++ b/aio/src/styles/ng-io-theme.scss @@ -1,4 +1,5 @@ @import '~@angular/material/theming'; +@import 'app-theme'; // Plus imports for other components in your app. // Include the base styles for Angular Material core. We include this here so that you only @@ -21,3 +22,4 @@ $ng-io-theme: mat-light-theme($ng-io-primary, $ng-io-accent, $ng-io-warn); // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include angular-material-theme($ng-io-theme); +@include app-theme($ng-io-theme); \ No newline at end of file