Skip to content

Commit fb3af01

Browse files
committed
refactor: add themeable base class
* Introduces a new `MdThemeable` base class that can be extended by different components to automatically support the `color` input. * This reduces a lot of repeated code in the different components and it also simplifies maintaining. Closes #2394.
1 parent 1ab1af1 commit fb3af01

File tree

10 files changed

+136
-185
lines changed

10 files changed

+136
-185
lines changed

src/lib/button/button.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@angular/core';
1212
import {CommonModule} from '@angular/common';
1313
import {MdRippleModule, coerceBooleanProperty, CompatibilityModule} from '../core';
14+
import {MdThemeable} from '../core/style/themeable';
1415

1516

1617
// TODO(jelbourn): Make the `isMouseDown` stuff done with one global listener.
@@ -97,8 +98,7 @@ export class MdMiniFabCssMatStyler {}
9798
encapsulation: ViewEncapsulation.None,
9899
changeDetection: ChangeDetectionStrategy.OnPush,
99100
})
100-
export class MdButton {
101-
private _color: string;
101+
export class MdButton extends MdThemeable {
102102

103103
/** Whether the button has focus from the keyboard (not the mouse). Used for class binding. */
104104
_isKeyboardFocused: boolean = false;
@@ -120,12 +120,9 @@ export class MdButton {
120120
get disabled() { return this._disabled; }
121121
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value) ? true : null; }
122122

123-
constructor(private _elementRef: ElementRef, private _renderer: Renderer) { }
124-
125-
/** The color of the button. Can be `primary`, `accent`, or `warn`. */
126-
@Input()
127-
get color(): string { return this._color; }
128-
set color(value: string) { this._updateColor(value); }
123+
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
124+
super(_renderer, _elementRef);
125+
}
129126

130127
_setMousedown() {
131128
// We only *show* the focus style when focus has come to the button via the keyboard.
@@ -136,18 +133,6 @@ export class MdButton {
136133
setTimeout(() => { this._isMouseDown = false; }, 100);
137134
}
138135

139-
_updateColor(newColor: string) {
140-
this._setElementColor(this._color, false);
141-
this._setElementColor(newColor, true);
142-
this._color = newColor;
143-
}
144-
145-
_setElementColor(color: string, isAdd: boolean) {
146-
if (color != null && color != '') {
147-
this._renderer.setElementClass(this._getHostElement(), `mat-${color}`, isAdd);
148-
}
149-
}
150-
151136
_setKeyboardFocus() {
152137
this._isKeyboardFocused = !this._isMouseDown;
153138
}

src/lib/checkbox/checkbox.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {CommonModule} from '@angular/common';
1717
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
1818
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
1919
import {MdRippleModule, CompatibilityModule} from '../core';
20+
import {MdThemeable} from '../core/style/themeable';
2021

2122

2223
/** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */
@@ -79,7 +80,7 @@ export class MdCheckboxChange {
7980
encapsulation: ViewEncapsulation.None,
8081
changeDetection: ChangeDetectionStrategy.OnPush
8182
})
82-
export class MdCheckbox implements ControlValueAccessor {
83+
export class MdCheckbox extends MdThemeable implements ControlValueAccessor {
8384
/**
8485
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
8586
* take precedence so this may be omitted.
@@ -168,15 +169,16 @@ export class MdCheckbox implements ControlValueAccessor {
168169

169170
private _indeterminate: boolean = false;
170171

171-
private _color: string;
172-
173172
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
174173

175174
_hasFocus: boolean = false;
176175

177176
constructor(private _renderer: Renderer,
178177
private _elementRef: ElementRef,
179178
private _changeDetectorRef: ChangeDetectorRef) {
179+
super(_renderer, _elementRef);
180+
181+
// By default set the component color to accent.
180182
this.color = 'accent';
181183
}
182184

@@ -228,23 +230,6 @@ export class MdCheckbox implements ControlValueAccessor {
228230
}
229231
}
230232

231-
/** The color of the button. Can be `primary`, `accent`, or `warn`. */
232-
@Input()
233-
get color(): string { return this._color; }
234-
set color(value: string) { this._updateColor(value); }
235-
236-
_updateColor(newColor: string) {
237-
this._setElementColor(this._color, false);
238-
this._setElementColor(newColor, true);
239-
this._color = newColor;
240-
}
241-
242-
_setElementColor(color: string, isAdd: boolean) {
243-
if (color != null && color != '') {
244-
this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
245-
}
246-
}
247-
248233
_isRippleDisabled() {
249234
return this.disableRipple || this.disabled;
250235
}

src/lib/chips/chip.ts

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111

1212
import {Focusable} from '../core/a11y/focus-key-manager';
1313
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
14+
import {MdThemeable} from '../core/style/themeable';
1415

1516
export interface MdChipEvent {
1617
chip: MdChip;
@@ -35,17 +36,14 @@ export interface MdChipEvent {
3536
'(click)': '_handleClick($event)'
3637
}
3738
})
38-
export class MdChip implements Focusable, OnInit, OnDestroy {
39+
export class MdChip extends MdThemeable implements Focusable, OnInit, OnDestroy {
3940

4041
/** Whether or not the chip is disabled. Disabled chips cannot be focused. */
4142
protected _disabled: boolean = null;
4243

4344
/** Whether or not the chip is selected. */
4445
protected _selected: boolean = false;
4546

46-
/** The palette color of selected chips. */
47-
protected _color: string = 'primary';
48-
4947
/** Emitted when the chip is focused. */
5048
onFocus = new EventEmitter<MdChipEvent>();
5149

@@ -58,11 +56,15 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
5856
/** Emitted when the chip is destroyed. */
5957
@Output() destroy = new EventEmitter<MdChipEvent>();
6058

61-
constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) { }
59+
constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {
60+
super(_renderer, _elementRef);
61+
62+
// By default the chip elements should use the primary palette.
63+
this.color = 'primary';
64+
}
6265

6366
ngOnInit(): void {
6467
this._addDefaultCSSClass();
65-
this._updateColor(this._color);
6668
}
6769

6870
ngOnDestroy(): void {
@@ -108,15 +110,6 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
108110
return this.selected;
109111
}
110112

111-
/** The color of the chip. Can be `primary`, `accent`, or `warn`. */
112-
@Input() get color(): string {
113-
return this._color;
114-
}
115-
116-
set color(value: string) {
117-
this._updateColor(value);
118-
}
119-
120113
/** Allows for programmatic focusing of the chip. */
121114
focus(): void {
122115
this._renderer.invokeElementMethod(this._elementRef.nativeElement, 'focus');
@@ -148,17 +141,4 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
148141
}
149142
}
150143

151-
/** Updates the private _color variable and the native element. */
152-
private _updateColor(newColor: string) {
153-
this._setElementColor(this._color, false);
154-
this._setElementColor(newColor, true);
155-
this._color = newColor;
156-
}
157-
158-
/** Sets the mat-color on the native element. */
159-
private _setElementColor(color: string, isAdd: boolean) {
160-
if (color != null && color != '') {
161-
this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
162-
}
163-
}
164144
}

src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ElementRef,
66
Renderer,
77
} from '@angular/core';
8+
import {MdThemeable} from '../../style/themeable';
89

910
export type MdPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
1011

@@ -32,29 +33,12 @@ export type MdPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
3233
'[class.mat-pseudo-checkbox-disabled]': 'disabled',
3334
},
3435
})
35-
export class MdPseudoCheckbox {
36+
export class MdPseudoCheckbox extends MdThemeable {
3637
/** Display state of the checkbox. */
3738
@Input() state: MdPseudoCheckboxState = 'unchecked';
3839

39-
/** Whether the checkbox is disabled. */
40-
@Input() disabled: boolean = false;
41-
42-
/** Color of the checkbox. */
43-
@Input()
44-
get color(): string { return this._color; };
45-
set color(value: string) {
46-
if (value) {
47-
let nativeElement = this._elementRef.nativeElement;
48-
49-
this._renderer.setElementClass(nativeElement, `mat-${this.color}`, false);
50-
this._renderer.setElementClass(nativeElement, `mat-${value}`, true);
51-
this._color = value;
52-
}
53-
}
54-
55-
private _color: string;
56-
57-
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
40+
constructor(elementRef: ElementRef, renderer: Renderer) {
41+
super(renderer, elementRef);
5842
this.color = 'accent';
5943
}
6044
}

src/lib/core/style/themeable.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {Component, ElementRef, Renderer} from '@angular/core';
3+
import {MdThemeable} from './themeable';
4+
import {By} from '@angular/platform-browser';
5+
6+
describe('MdThemeable', () => {
7+
8+
let fixture: ComponentFixture<TestComponent>;
9+
let testComponent: TestComponent;
10+
let themeableElement: HTMLElement;
11+
12+
beforeEach(async(() => {
13+
TestBed.configureTestingModule({
14+
declarations: [TestComponent, ThemeableComponent],
15+
});
16+
17+
TestBed.compileComponents();
18+
}));
19+
20+
beforeEach(() => {
21+
fixture = TestBed.createComponent(TestComponent);
22+
fixture.detectChanges();
23+
24+
testComponent = fixture.componentInstance;
25+
themeableElement = fixture.debugElement.query(By.css('themeable-test')).nativeElement;
26+
});
27+
28+
it('should support a default component color', () => {
29+
expect(themeableElement.classList).toContain('mat-warn');
30+
});
31+
32+
it('should update classes on color change', () => {
33+
expect(themeableElement.classList).toContain('mat-warn');
34+
35+
testComponent.color = 'primary';
36+
fixture.detectChanges();
37+
38+
expect(themeableElement.classList).toContain('mat-primary');
39+
expect(themeableElement.classList).not.toContain('mat-warn');
40+
41+
testComponent.color = 'accent';
42+
fixture.detectChanges();
43+
44+
expect(themeableElement.classList).toContain('mat-accent');
45+
expect(themeableElement.classList).not.toContain('mat-warn');
46+
expect(themeableElement.classList).not.toContain('mat-primary');
47+
});
48+
49+
});
50+
51+
@Component({
52+
selector: 'themeable-test',
53+
template: '<span>Themeable</span>'
54+
})
55+
class ThemeableComponent extends MdThemeable {
56+
constructor(renderer: Renderer, elementRef: ElementRef) {
57+
super(renderer, elementRef);
58+
}
59+
}
60+
61+
@Component({
62+
template: '<themeable-test [color]="color"></themeable-test>'
63+
})
64+
class TestComponent {
65+
color: string = 'warn';
66+
}

src/lib/core/style/themeable.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {ElementRef, Input, Renderer} from '@angular/core';
2+
3+
/** Material components can extend the MdThemeable class to add an Input that can
4+
* developers use to switch palettes on the components. */
5+
export class MdThemeable {
6+
7+
/** Stored color for the themeable component. */
8+
private _color: string;
9+
10+
constructor(private renderer: Renderer, private elementRef: ElementRef) {}
11+
12+
/** Color of the component. Values are primary, accent, or warn. */
13+
@Input()
14+
get color(): string {
15+
return this._color;
16+
}
17+
set color(newColor: string) {
18+
this._setElementColor(this._color, false);
19+
this._setElementColor(newColor, true);
20+
this._color = newColor;
21+
}
22+
23+
/** Toggles a color class on the components host element. */
24+
private _setElementColor(color: string, isAdd: boolean) {
25+
if (color != null && color != '') {
26+
this.renderer.setElementClass(this.elementRef.nativeElement, `mat-${color}`, isAdd);
27+
}
28+
}
29+
30+
}

src/lib/icon/icon.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {HttpModule, Http} from '@angular/http';
1818
import {DomSanitizer} from '@angular/platform-browser';
1919
import {MdError, CompatibilityModule} from '../core';
2020
import {MdIconRegistry} from './icon-registry';
21+
import {MdThemeable} from '../core/style/themeable';
2122
export {MdIconRegistry} from './icon-registry';
2223

2324
/** Exception thrown when an invalid icon name is passed to an md-icon component. */
@@ -72,8 +73,7 @@ export class MdIconInvalidNameError extends MdError {
7273
encapsulation: ViewEncapsulation.None,
7374
changeDetection: ChangeDetectionStrategy.OnPush,
7475
})
75-
export class MdIcon implements OnChanges, OnInit, AfterViewChecked {
76-
private _color: string;
76+
export class MdIcon extends MdThemeable implements OnChanges, OnInit, AfterViewChecked {
7777

7878
/** Name of the icon in the SVG icon set. */
7979
@Input() svgIcon: string;
@@ -90,30 +90,15 @@ export class MdIcon implements OnChanges, OnInit, AfterViewChecked {
9090
/** Screenreader label for the icon. */
9191
@Input('aria-label') hostAriaLabel: string = '';
9292

93-
/** Color of the icon. */
94-
@Input()
95-
get color(): string { return this._color; }
96-
set color(value: string) { this._updateColor(value); }
97-
9893
private _previousFontSetClass: string;
9994
private _previousFontIconClass: string;
10095
private _previousAriaLabel: string;
10196

10297
constructor(
10398
private _elementRef: ElementRef,
10499
private _renderer: Renderer,
105-
private _mdIconRegistry: MdIconRegistry) { }
106-
107-
_updateColor(newColor: string) {
108-
this._setElementColor(this._color, false);
109-
this._setElementColor(newColor, true);
110-
this._color = newColor;
111-
}
112-
113-
_setElementColor(color: string, isAdd: boolean) {
114-
if (color != null && color != '') {
115-
this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
116-
}
100+
private _mdIconRegistry: MdIconRegistry) {
101+
super(_renderer, _elementRef);
117102
}
118103

119104
/**

0 commit comments

Comments
 (0)