Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit 74eb084

Browse files
mmalerbariavalon
authored andcommitted
feat(navbar): Add themepicker component with lazy loaded themes
1 parent 1bccf71 commit 74eb084

19 files changed

+282
-4
lines changed

angular-cli.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
"mobile": false,
2020
"styles": [
2121
"main.scss",
22-
"highlightjs/solarized-light.css"
22+
"highlightjs/solarized-light.css",
23+
{"input": "assets/pink-bluegrey.css", "lazy": true},
24+
{"input": "assets/deeppurple-amber.css", "lazy": true},
25+
{"input": "assets/indigo-pink.css", "lazy": true},
26+
{"input": "assets/purple-green.css", "lazy": true}
2327
],
2428
"scripts": [],
2529
"environmentSource": "environments/environment.ts",

src/app/app-module.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
12
import {BrowserModule} from '@angular/platform-browser';
23
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
34
import {NgModule} from '@angular/core';
@@ -19,6 +20,7 @@ import {ComponentSidenav} from './pages/component-sidenav/component-sidenav';
1920
import {Footer} from './shared/footer/footer';
2021
import {ComponentPageTitle} from './pages/page-title/page-title';
2122
import {ComponentPageHeader} from './pages/component-page-header/component-page-header';
23+
import {StyleManager} from './shared/style-manager/style-manager';
2224

2325

2426
@NgModule({
@@ -32,7 +34,14 @@ import {ComponentPageHeader} from './pages/component-page-header/component-page-
3234
GuideList,
3335
GuideViewer,
3436
Homepage,
35-
Footer
37+
Footer,
38+
],
39+
schemas: [
40+
CUSTOM_ELEMENTS_SCHEMA,
41+
],
42+
exports: [
43+
MaterialDocsApp,
44+
Homepage,
3645
],
3746
imports: [
3847
BrowserModule,
@@ -47,6 +56,7 @@ import {ComponentPageHeader} from './pages/component-page-header/component-page-
4756
providers: [
4857
Location,
4958
ComponentPageTitle,
59+
StyleManager,
5060
{provide: LocationStrategy, useClass: PathLocationStrategy},
5161
],
5262
bootstrap: [MaterialDocsApp],

src/app/shared/navbar/navbar.html

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
</a>
99
<a md-button class="docs-button" routerLink="components">Components</a>
1010
<a md-button class="docs-button" routerLink="guides">Guides</a>
11+
<div class="flex-spacer"></div>
12+
<theme-chooser></theme-chooser>
1113
<a md-button class="docs-button" href="https://github.com/angular/material2" aria-label="GitHub Repository">
1214
<img class="docs-github-logo"
1315
src="../../../assets/img/homepage/github-circle-white-transparent.svg"

src/app/shared/navbar/navbar.scss

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.docs-navbar {
22
display: flex;
33
flex-wrap: wrap;
4+
align-items: center;
45
padding: 8px 16px;
56

67
> .mat-button {
@@ -21,3 +22,7 @@
2122
margin: 0 7px 2px 0;
2223
vertical-align: middle;
2324
}
25+
26+
.flex-spacer {
27+
flex-grow: 1;
28+
}

src/app/shared/shared-module.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {BrowserModule} from '@angular/platform-browser';
99
import {RouterModule} from '@angular/router';
1010
import {PlunkerButton} from './plunker';
1111
import {GuideItems} from './guide-items/guide-items';
12+
import {ThemeChooser} from './theme-chooser/theme-chooser';
1213

1314

1415
@NgModule({
@@ -18,8 +19,8 @@ import {GuideItems} from './guide-items/guide-items';
1819
BrowserModule,
1920
MaterialModule,
2021
],
21-
declarations: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
22-
exports: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
22+
declarations: [DocViewer, ExampleViewer, NavBar, PlunkerButton, ThemeChooser],
23+
exports: [DocViewer, ExampleViewer, NavBar, PlunkerButton, ThemeChooser],
2324
providers: [DocumentationItems, GuideItems],
2425
entryComponents: [
2526
ExampleViewer,

src/app/shared/style-manager/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './style-manager';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {TestBed, inject} from '@angular/core/testing';
2+
import {StyleManager} from './style-manager';
3+
4+
5+
describe('StyleManager', () => {
6+
let styleManager: StyleManager;
7+
8+
beforeEach(() => TestBed.configureTestingModule({
9+
providers: [StyleManager]
10+
}));
11+
12+
beforeEach(inject([StyleManager], (sm: StyleManager) => {
13+
styleManager = sm;
14+
}));
15+
16+
afterEach(() => {
17+
let links = document.head.querySelectorAll('link');
18+
for (let link of Array.prototype.slice.call(links)) {
19+
if (link.className.includes('style-manager-')) {
20+
document.head.removeChild(link);
21+
}
22+
}
23+
});
24+
25+
it('should add stylesheet to head', () => {
26+
styleManager.setStyle('test', 'test.css');
27+
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
28+
expect(styleEl).not.toBeNull();
29+
expect(styleEl.href.endsWith('test.css')).toBe(true);
30+
});
31+
32+
it('should change existing stylesheet', () => {
33+
styleManager.setStyle('test', 'test.css');
34+
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
35+
expect(styleEl).not.toBeNull();
36+
expect(styleEl.href.endsWith('test.css')).toBe(true);
37+
38+
styleManager.setStyle('test', 'new.css');
39+
expect(styleEl.href.endsWith('new.css')).toBe(true);
40+
});
41+
42+
it('should remove existing stylesheet', () => {
43+
styleManager.setStyle('test', 'test.css');
44+
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
45+
expect(styleEl).not.toBeNull();
46+
expect(styleEl.href.endsWith('test.css')).toBe(true);
47+
48+
styleManager.removeStyle('test');
49+
styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
50+
expect(styleEl).toBeNull();
51+
});
52+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {Injectable} from '@angular/core';
2+
3+
4+
/**
5+
* Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be
6+
* removed or changed later.
7+
*/
8+
@Injectable()
9+
export class StyleManager {
10+
/**
11+
* Set the stylesheet with the specified key.
12+
*/
13+
setStyle(key: string, href: string) {
14+
getLinkElementForKey(key).setAttribute('href', href);
15+
}
16+
17+
/**
18+
* Remove the stylesheet with the specified key.
19+
*/
20+
removeStyle(key: string) {
21+
const existingLinkElement = getExistingLinkElementByKey(key);
22+
if (existingLinkElement) {
23+
document.head.removeChild(existingLinkElement);
24+
}
25+
}
26+
}
27+
28+
function getLinkElementForKey(key: string) {
29+
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
30+
}
31+
32+
function getExistingLinkElementByKey(key: string) {
33+
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
34+
}
35+
36+
function createLinkElementWithKey(key: string) {
37+
const linkEl = document.createElement('link');
38+
linkEl.setAttribute('rel', 'stylesheet');
39+
linkEl.classList.add(getClassNameForKey(key));
40+
document.head.appendChild(linkEl);
41+
return linkEl;
42+
}
43+
44+
function getClassNameForKey(key: string) {
45+
return `style-manager-${key}`;
46+
}

src/app/shared/theme-chooser/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './theme-chooser';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<button md-icon-button [md-menu-trigger-for]="themeMenu">
2+
<md-icon>format_color_fill</md-icon>
3+
</button>
4+
5+
<md-menu class="docs-theme-chooser-menu" #themeMenu="mdMenu" x-position="before">
6+
<md-grid-list cols="2">
7+
<md-grid-tile *ngFor="let theme of themes">
8+
<div md-menu-item (click)="installTheme(theme.href)">
9+
<div class="docs-theme-chooser-swatch">
10+
<md-icon class="docs-theme-chosen-icon" *ngIf="currentTheme === theme.href">check_circle</md-icon>
11+
<div class="docs-theme-chooser-primary" [style.background]="theme.primary"></div>
12+
<!--<div class="docs-theme-chooser-accent" [style.background]="theme.accent"></div>-->
13+
</div>
14+
</div>
15+
</md-grid-tile>
16+
</md-grid-list>
17+
</md-menu>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
$theme-chooser-menu-padding: 8px;
2+
$theme-chooser-grid-cell-size: 48px;
3+
$theme-chooser-grid-cells-per-row: 2;
4+
$theme-chooser-swatch-size: 36px;
5+
$theme-chooser-accent-stripe-size: 6px;
6+
7+
8+
.docs-theme-chooser-menu {
9+
.md-menu-content {
10+
padding: $theme-chooser-menu-padding;
11+
}
12+
13+
[md-menu-item] {
14+
flex: 0 0 auto;
15+
padding: 0;
16+
overflow: hidden;
17+
}
18+
19+
.docs-theme-chooser-swatch {
20+
position: relative;
21+
width: $theme-chooser-swatch-size;
22+
height: $theme-chooser-swatch-size;
23+
margin: ($theme-chooser-grid-cell-size - $theme-chooser-swatch-size) / 2;
24+
border-radius: 50%;
25+
overflow: hidden;
26+
27+
.docs-theme-chosen-icon {
28+
color: white;
29+
position: absolute;
30+
left: 50%; top: 50%;
31+
transform: translate(-50%, -50%);
32+
}
33+
34+
&::after {
35+
content: '';
36+
position: absolute;
37+
top: 0;
38+
left: 0;
39+
width: 100%;
40+
height: 100%;
41+
box-sizing: border-box;
42+
border: 1px solid rgba(0,0,0,.2);
43+
border-radius: 50%;
44+
}
45+
}
46+
47+
.docs-theme-chooser-primary {
48+
width: 100%;
49+
height: 100%;
50+
}
51+
52+
.docs-theme-chooser-accent {
53+
position: absolute;
54+
bottom: $theme-chooser-accent-stripe-size;
55+
width: 100%;
56+
height: $theme-chooser-accent-stripe-size;
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {MaterialModule} from '@angular/material';
2+
import {async, TestBed} from '@angular/core/testing';
3+
4+
import {ThemeChooser} from './theme-chooser';
5+
import {StyleManager} from '../style-manager';
6+
7+
8+
describe('ThemeChooser', () => {
9+
beforeEach(async(() => {
10+
TestBed.configureTestingModule({
11+
imports: [MaterialModule],
12+
declarations: [ThemeChooser],
13+
providers: [StyleManager]
14+
});
15+
16+
TestBed.compileComponents();
17+
}));
18+
19+
it('should install theme based on href', () => {
20+
const fixture = TestBed.createComponent(ThemeChooser);
21+
const component = fixture.componentInstance;
22+
const href = 'assets/pink-bluegrey.css';
23+
spyOn(component.styleManager, 'setStyle');
24+
component.installTheme(href);
25+
expect(component.styleManager.setStyle).toHaveBeenCalled();
26+
expect(component.styleManager.setStyle).toHaveBeenCalledWith('theme', href);
27+
});
28+
});
29+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {Component, ViewEncapsulation, ChangeDetectionStrategy} from '@angular/core';
2+
import {StyleManager} from '../style-manager/style-manager';
3+
4+
@Component({
5+
selector: 'theme-chooser',
6+
templateUrl: 'theme-chooser.html',
7+
styleUrls: ['theme-chooser.scss'],
8+
changeDetection: ChangeDetectionStrategy.OnPush,
9+
encapsulation: ViewEncapsulation.None,
10+
host: { 'aria-hidden': 'true' },
11+
})
12+
export class ThemeChooser {
13+
currentTheme;
14+
15+
themes = [
16+
{
17+
primary: '#673AB7',
18+
accent: '#FFC107',
19+
href: 'deeppurple-amber.css'
20+
},
21+
{
22+
primary: '#3F51B5',
23+
accent: '#E91E63',
24+
href: 'indigo-pink.css'
25+
},
26+
{
27+
primary: '#E91E63',
28+
accent: '#607D8B',
29+
href: 'pink-bluegrey.css'
30+
},
31+
{
32+
primary: '#9C27B0',
33+
accent: '#4CAF50',
34+
href: 'purple-green.css'
35+
},
36+
];
37+
38+
constructor(public styleManager : StyleManager) {}
39+
40+
installTheme(href: string) {
41+
this.currentTheme = href;
42+
this.styleManager.setStyle('theme', `assets/${href}`);
43+
}
44+
}

src/app/shared/theme-chooser/theme-storage/theme-storage.spec.ts

Whitespace-only changes.

src/app/shared/theme-chooser/theme-storage/theme-storage.ts

Whitespace-only changes.

src/assets/deeppurple-amber.css

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/assets/indigo-pink.css

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/assets/pink-bluegrey.css

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/assets/purple-green.css

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)