Skip to content

Commit 705519d

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 3ad6ff0 commit 705519d

File tree

11 files changed

+192
-192
lines changed

11 files changed

+192
-192
lines changed

src/demo-app/dialog/dialog-demo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class JazzDialog {
119119
120120
<button
121121
md-button
122-
color="secondary"
122+
color="accent"
123123
(click)="showInStackedDialog()">
124124
Show in Dialog</button>
125125
</md-dialog-actions>

src/lib/button/button.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ViewEncapsulation
1111
} from '@angular/core';
1212
import {coerceBooleanProperty, FocusOriginMonitor} from '../core';
13+
import {MdThemeable} from '../core/style/themeable';
1314

1415

1516
// TODO(kara): Convert attribute selectors to classes when attr maps become available
@@ -96,8 +97,7 @@ export class MdMiniFabCssMatStyler {}
9697
encapsulation: ViewEncapsulation.None,
9798
changeDetection: ChangeDetectionStrategy.OnPush,
9899
})
99-
export class MdButton implements OnDestroy {
100-
private _color: string;
100+
export class MdButton extends MdThemeable implements OnDestroy {
101101

102102
/** Whether the button is round. */
103103
_isRoundButton: boolean = ['icon-button', 'fab', 'mini-fab'].some(suffix => {
@@ -119,32 +119,16 @@ export class MdButton implements OnDestroy {
119119
get disabled() { return this._disabled; }
120120
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value) ? true : null; }
121121

122-
constructor(private _elementRef: ElementRef, private _renderer: Renderer,
123-
private _focusOriginMonitor: FocusOriginMonitor) {
124-
this._focusOriginMonitor.monitor(this._elementRef.nativeElement, this._renderer, true);
122+
constructor(private _focusOriginMonitor: FocusOriginMonitor, elementRef: ElementRef,
123+
renderer: Renderer) {
124+
super(renderer, elementRef);
125+
this._focusOriginMonitor.monitor(elementRef.nativeElement, renderer, true);
125126
}
126127

127128
ngOnDestroy() {
128129
this._focusOriginMonitor.unmonitor(this._elementRef.nativeElement);
129130
}
130131

131-
/** The color of the button. Can be `primary`, `accent`, or `warn`. */
132-
@Input()
133-
get color(): string { return this._color; }
134-
set color(value: string) { this._updateColor(value); }
135-
136-
_updateColor(newColor: string) {
137-
this._setElementColor(this._color, false);
138-
this._setElementColor(newColor, true);
139-
this._color = newColor;
140-
}
141-
142-
_setElementColor(color: string, isAdd: boolean) {
143-
if (color != null && color != '') {
144-
this._renderer.setElementClass(this._getHostElement(), `mat-${color}`, isAdd);
145-
}
146-
}
147-
148132
/** Focuses the button. */
149133
focus(): void {
150134
this._renderer.invokeElementMethod(this._getHostElement(), 'focus');
@@ -177,7 +161,7 @@ export class MdButton implements OnDestroy {
177161
})
178162
export class MdAnchor extends MdButton {
179163
constructor(elementRef: ElementRef, renderer: Renderer, focusOriginMonitor: FocusOriginMonitor) {
180-
super(elementRef, renderer, focusOriginMonitor);
164+
super(focusOriginMonitor, elementRef, renderer);
181165
}
182166

183167
/** @docs-private */

src/lib/checkbox/checkbox.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
RippleRef,
2222
FocusOriginMonitor,
2323
} from '../core';
24+
import {MdThemeable} from '../core/style/themeable';
2425

2526

2627
/** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */
@@ -84,7 +85,9 @@ export class MdCheckboxChange {
8485
encapsulation: ViewEncapsulation.None,
8586
changeDetection: ChangeDetectionStrategy.OnPush
8687
})
87-
export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestroy {
88+
export class MdCheckbox extends MdThemeable
89+
implements ControlValueAccessor, AfterViewInit, OnDestroy {
90+
8891
/**
8992
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
9093
* take precedence so this may be omitted.
@@ -178,8 +181,6 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
178181

179182
private _indeterminate: boolean = false;
180183

181-
private _color: string;
182-
183184
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
184185

185186
/** Reference to the focused state ripple. */
@@ -188,10 +189,13 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
188189
/** Reference to the focus origin monitor subscription. */
189190
private _focusedSubscription: Subscription;
190191

191-
constructor(private _renderer: Renderer,
192-
private _elementRef: ElementRef,
193-
private _changeDetectorRef: ChangeDetectorRef,
194-
private _focusOriginMonitor: FocusOriginMonitor) {
192+
constructor(
193+
private _changeDetectorRef: ChangeDetectorRef,
194+
private _focusOriginMonitor: FocusOriginMonitor,
195+
renderer: Renderer,
196+
elementRef: ElementRef
197+
) {
198+
super(renderer, elementRef);
195199
this.color = 'accent';
196200
}
197201

@@ -261,23 +265,6 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
261265
}
262266
}
263267

264-
/** The color of the button. Can be `primary`, `accent`, or `warn`. */
265-
@Input()
266-
get color(): string { return this._color; }
267-
set color(value: string) { this._updateColor(value); }
268-
269-
_updateColor(newColor: string) {
270-
this._setElementColor(this._color, false);
271-
this._setElementColor(newColor, true);
272-
this._color = newColor;
273-
}
274-
275-
_setElementColor(color: string, isAdd: boolean) {
276-
if (color != null && color != '') {
277-
this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
278-
}
279-
}
280-
281268
_isRippleDisabled() {
282269
return this.disableRipple || this.disabled;
283270
}

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(renderer: Renderer, 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 & 17 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,15 @@ 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

3940
/** Whether the checkbox is disabled. */
4041
@Input() disabled: boolean = false;
4142

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) {
43+
constructor(elementRef: ElementRef, renderer: Renderer) {
44+
super(renderer, elementRef);
5845
this.color = 'accent';
5946
}
6047
}

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
testComponent.color = null;
49+
fixture.detectChanges();
50+
51+
expect(themeableElement.classList).not.toContain('mat-accent');
52+
expect(themeableElement.classList).not.toContain('mat-warn');
53+
expect(themeableElement.classList).not.toContain('mat-primary');
54+
});
55+
56+
it('should throw an error when using an invalid color', () => {
57+
testComponent.color = 'Invalid';
58+
59+
expect(() => fixture.detectChanges()).toThrow();
60+
});
61+
62+
});
63+
64+
@Component({
65+
selector: 'themeable-test',
66+
template: '<span>Themeable</span>'
67+
})
68+
class ThemeableComponent extends MdThemeable {
69+
constructor(renderer: Renderer, elementRef: ElementRef) {
70+
super(renderer, elementRef);
71+
}
72+
}
73+
74+
@Component({
75+
template: '<themeable-test [color]="color"></themeable-test>'
76+
})
77+
class TestComponent {
78+
color: string = 'warn';
79+
}

0 commit comments

Comments
 (0)