diff --git a/src/demo-app/dialog/dialog-demo.ts b/src/demo-app/dialog/dialog-demo.ts
index f82ecbfbce4c..0f4ffbd5c9ef 100644
--- a/src/demo-app/dialog/dialog-demo.ts
+++ b/src/demo-app/dialog/dialog-demo.ts
@@ -119,7 +119,7 @@ export class JazzDialog {
diff --git a/src/lib/button/button.ts b/src/lib/button/button.ts
index 5b5ae5370157..3104ce791b74 100644
--- a/src/lib/button/button.ts
+++ b/src/lib/button/button.ts
@@ -10,6 +10,7 @@ import {
ViewEncapsulation
} from '@angular/core';
import {coerceBooleanProperty, FocusOriginMonitor} from '../core';
+import {MdThemeable} from '../core/style/themeable';
// TODO(kara): Convert attribute selectors to classes when attr maps become available
@@ -96,8 +97,7 @@ export class MdMiniFabCssMatStyler {}
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MdButton implements OnDestroy {
- private _color: string;
+export class MdButton extends MdThemeable implements OnDestroy {
/** Whether the button is round. */
_isRoundButton: boolean = ['icon-button', 'fab', 'mini-fab'].some(suffix => {
@@ -119,32 +119,16 @@ export class MdButton implements OnDestroy {
get disabled() { return this._disabled; }
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value) ? true : null; }
- constructor(private _elementRef: ElementRef, private _renderer: Renderer,
- private _focusOriginMonitor: FocusOriginMonitor) {
- this._focusOriginMonitor.monitor(this._elementRef.nativeElement, this._renderer, true);
+ constructor(private _focusOriginMonitor: FocusOriginMonitor, elementRef: ElementRef,
+ renderer: Renderer) {
+ super(renderer, elementRef);
+ this._focusOriginMonitor.monitor(elementRef.nativeElement, renderer, true);
}
ngOnDestroy() {
this._focusOriginMonitor.unmonitor(this._elementRef.nativeElement);
}
- /** The color of the button. Can be `primary`, `accent`, or `warn`. */
- @Input()
- get color(): string { return this._color; }
- set color(value: string) { this._updateColor(value); }
-
- _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._getHostElement(), `mat-${color}`, isAdd);
- }
- }
-
/** Focuses the button. */
focus(): void {
this._renderer.invokeElementMethod(this._getHostElement(), 'focus');
@@ -177,7 +161,7 @@ export class MdButton implements OnDestroy {
})
export class MdAnchor extends MdButton {
constructor(elementRef: ElementRef, renderer: Renderer, focusOriginMonitor: FocusOriginMonitor) {
- super(elementRef, renderer, focusOriginMonitor);
+ super(focusOriginMonitor, elementRef, renderer);
}
/** @docs-private */
diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts
index 8ff44319a258..26bca2529c26 100644
--- a/src/lib/checkbox/checkbox.ts
+++ b/src/lib/checkbox/checkbox.ts
@@ -21,6 +21,7 @@ import {
RippleRef,
FocusOriginMonitor,
} from '../core';
+import {MdThemeable} from '../core/style/themeable';
/** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */
@@ -84,7 +85,9 @@ export class MdCheckboxChange {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestroy {
+export class MdCheckbox extends MdThemeable
+ implements ControlValueAccessor, AfterViewInit, OnDestroy {
+
/**
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
* take precedence so this may be omitted.
@@ -178,8 +181,6 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
private _indeterminate: boolean = false;
- private _color: string;
-
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
/** Reference to the focused state ripple. */
@@ -188,10 +189,13 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
/** Reference to the focus origin monitor subscription. */
private _focusedSubscription: Subscription;
- constructor(private _renderer: Renderer,
- private _elementRef: ElementRef,
- private _changeDetectorRef: ChangeDetectorRef,
- private _focusOriginMonitor: FocusOriginMonitor) {
+ constructor(
+ private _changeDetectorRef: ChangeDetectorRef,
+ private _focusOriginMonitor: FocusOriginMonitor,
+ renderer: Renderer,
+ elementRef: ElementRef
+ ) {
+ super(renderer, elementRef);
this.color = 'accent';
}
@@ -261,23 +265,6 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
}
}
- /** The color of the button. Can be `primary`, `accent`, or `warn`. */
- @Input()
- get color(): string { return this._color; }
- set color(value: string) { this._updateColor(value); }
-
- _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
- }
- }
-
_isRippleDisabled() {
return this.disableRipple || this.disabled;
}
diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts
index 73656bdfefdf..1fd38d4dce21 100644
--- a/src/lib/chips/chip.ts
+++ b/src/lib/chips/chip.ts
@@ -11,6 +11,7 @@ import {
import {Focusable} from '../core/a11y/focus-key-manager';
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
+import {MdThemeable} from '../core/style/themeable';
export interface MdChipEvent {
chip: MdChip;
@@ -35,7 +36,7 @@ export interface MdChipEvent {
'(click)': '_handleClick($event)'
}
})
-export class MdChip implements Focusable, OnInit, OnDestroy {
+export class MdChip extends MdThemeable implements Focusable, OnInit, OnDestroy {
/** Whether or not the chip is disabled. Disabled chips cannot be focused. */
protected _disabled: boolean = null;
@@ -43,9 +44,6 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
/** Whether or not the chip is selected. */
protected _selected: boolean = false;
- /** The palette color of selected chips. */
- protected _color: string = 'primary';
-
/** Emitted when the chip is focused. */
onFocus = new EventEmitter();
@@ -58,11 +56,15 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
/** Emitted when the chip is destroyed. */
@Output() destroy = new EventEmitter();
- constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) { }
+ constructor(renderer: Renderer, elementRef: ElementRef) {
+ super(renderer, elementRef);
+
+ // By default the chip elements should use the primary palette.
+ this.color = 'primary';
+ }
ngOnInit(): void {
this._addDefaultCSSClass();
- this._updateColor(this._color);
}
ngOnDestroy(): void {
@@ -108,15 +110,6 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
return this.selected;
}
- /** The color of the chip. Can be `primary`, `accent`, or `warn`. */
- @Input() get color(): string {
- return this._color;
- }
-
- set color(value: string) {
- this._updateColor(value);
- }
-
/** Allows for programmatic focusing of the chip. */
focus(): void {
this._renderer.invokeElementMethod(this._elementRef.nativeElement, 'focus');
@@ -148,17 +141,4 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
}
}
- /** Updates the private _color variable and the native element. */
- private _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- /** Sets the mat-color on the native element. */
- private _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
- }
- }
}
diff --git a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts
index d98bcf1b55c6..73bdb5d4ec84 100644
--- a/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts
+++ b/src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts
@@ -5,6 +5,7 @@ import {
ElementRef,
Renderer,
} from '@angular/core';
+import {MdThemeable} from '../../style/themeable';
export type MdPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
@@ -32,29 +33,15 @@ export type MdPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate';
'[class.mat-pseudo-checkbox-disabled]': 'disabled',
},
})
-export class MdPseudoCheckbox {
+export class MdPseudoCheckbox extends MdThemeable {
/** Display state of the checkbox. */
@Input() state: MdPseudoCheckboxState = 'unchecked';
/** Whether the checkbox is disabled. */
@Input() disabled: boolean = false;
- /** Color of the checkbox. */
- @Input()
- get color(): string { return this._color; };
- set color(value: string) {
- if (value) {
- let nativeElement = this._elementRef.nativeElement;
-
- this._renderer.setElementClass(nativeElement, `mat-${this.color}`, false);
- this._renderer.setElementClass(nativeElement, `mat-${value}`, true);
- this._color = value;
- }
- }
-
- private _color: string;
-
- constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
+ constructor(elementRef: ElementRef, renderer: Renderer) {
+ super(renderer, elementRef);
this.color = 'accent';
}
}
diff --git a/src/lib/core/style/themeable.spec.ts b/src/lib/core/style/themeable.spec.ts
new file mode 100644
index 000000000000..29e67315dd71
--- /dev/null
+++ b/src/lib/core/style/themeable.spec.ts
@@ -0,0 +1,79 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Component, ElementRef, Renderer} from '@angular/core';
+import {MdThemeable} from './themeable';
+import {By} from '@angular/platform-browser';
+
+describe('MdThemeable', () => {
+
+ let fixture: ComponentFixture;
+ let testComponent: TestComponent;
+ let themeableElement: HTMLElement;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent, ThemeableComponent],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+
+ testComponent = fixture.componentInstance;
+ themeableElement = fixture.debugElement.query(By.css('themeable-test')).nativeElement;
+ });
+
+ it('should support a default component color', () => {
+ expect(themeableElement.classList).toContain('mat-warn');
+ });
+
+ it('should update classes on color change', () => {
+ expect(themeableElement.classList).toContain('mat-warn');
+
+ testComponent.color = 'primary';
+ fixture.detectChanges();
+
+ expect(themeableElement.classList).toContain('mat-primary');
+ expect(themeableElement.classList).not.toContain('mat-warn');
+
+ testComponent.color = 'accent';
+ fixture.detectChanges();
+
+ expect(themeableElement.classList).toContain('mat-accent');
+ expect(themeableElement.classList).not.toContain('mat-warn');
+ expect(themeableElement.classList).not.toContain('mat-primary');
+
+ testComponent.color = null;
+ fixture.detectChanges();
+
+ expect(themeableElement.classList).not.toContain('mat-accent');
+ expect(themeableElement.classList).not.toContain('mat-warn');
+ expect(themeableElement.classList).not.toContain('mat-primary');
+ });
+
+ it('should throw an error when using an invalid color', () => {
+ testComponent.color = 'Invalid';
+
+ expect(() => fixture.detectChanges()).toThrow();
+ });
+
+});
+
+@Component({
+ selector: 'themeable-test',
+ template: 'Themeable'
+})
+class ThemeableComponent extends MdThemeable {
+ constructor(renderer: Renderer, elementRef: ElementRef) {
+ super(renderer, elementRef);
+ }
+}
+
+@Component({
+ template: ''
+})
+class TestComponent {
+ color: string = 'warn';
+}
diff --git a/src/lib/core/style/themeable.ts b/src/lib/core/style/themeable.ts
new file mode 100644
index 000000000000..bad646aee743
--- /dev/null
+++ b/src/lib/core/style/themeable.ts
@@ -0,0 +1,62 @@
+import {ElementRef, Input, Renderer} from '@angular/core';
+import {MdError} from '../errors/error';
+
+/** Possible color values for the color input. */
+export type MdThemeColor = 'primary' | 'accent' | 'warn';
+
+const VALID_COLOR_VALUES = ['primary', 'accent', 'warn'];
+
+/**
+ * Material components can extend the MdThemeable class to add an Input that can
+ * developers use to switch palettes on the components.
+ **/
+export class MdThemeable {
+
+ /** Stored color for the themeable component. */
+ private _color: MdThemeColor;
+
+ // Constructor initializers need to have the `protected` modifier to avoid interferences.
+ // TypeScript throws if similar declarations, regardless of the modifier, have been
+ // found across the different classes. Because of that, the child classes should just use
+ // the protected properties from the superclass.
+ // TODO(devversion): revisit this once TypeScript v2.2.1 is being used.
+ constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {}
+
+ /** Color of the component. Values are primary, accent, or warn. */
+ @Input()
+ get color(): MdThemeColor {
+ return this._color;
+ }
+ set color(newColor: MdThemeColor) {
+ this._validateColor(newColor);
+
+ this._setElementColor(this._color, false);
+ this._setElementColor(newColor, true);
+ this._color = newColor;
+ }
+
+ /** Validates the specified color value and throws an error if invalid. */
+ private _validateColor(color: string) {
+ if (color && VALID_COLOR_VALUES.indexOf(color) === -1) {
+ throw new MdInvalidColorValueError(color);
+ }
+ }
+
+ /** Toggles a color class on the components host element. */
+ private _setElementColor(color: string, isAdd: boolean) {
+ if (color) {
+ this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
+ }
+ }
+
+}
+
+/** Error that will be thrown if the color input is set to an invalid value. */
+export class MdInvalidColorValueError extends MdError {
+ constructor(invalidColor: string) {
+ super(
+ `The color "${invalidColor}" for is not valid. ` +
+ `Possible values are: ${VALID_COLOR_VALUES.join(', ')} or null.`
+ );
+ }
+}
diff --git a/src/lib/icon/icon.ts b/src/lib/icon/icon.ts
index d9e0e038ca6d..42a740ca7974 100644
--- a/src/lib/icon/icon.ts
+++ b/src/lib/icon/icon.ts
@@ -16,6 +16,7 @@ import {Http} from '@angular/http';
import {DomSanitizer} from '@angular/platform-browser';
import {MdError} from '../core';
import {MdIconRegistry, MdIconNameNotFoundError} from './icon-registry';
+import {MdThemeable} from '../core/style/themeable';
/** Exception thrown when an invalid icon name is passed to an md-icon component. */
export class MdIconInvalidNameError extends MdError {
@@ -69,8 +70,7 @@ export class MdIconInvalidNameError extends MdError {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MdIcon implements OnChanges, OnInit, AfterViewChecked {
- private _color: string;
+export class MdIcon extends MdThemeable implements OnChanges, OnInit, AfterViewChecked {
/** Name of the icon in the SVG icon set. */
@Input() svgIcon: string;
@@ -87,30 +87,15 @@ export class MdIcon implements OnChanges, OnInit, AfterViewChecked {
/** Screenreader label for the icon. */
@Input('aria-label') hostAriaLabel: string = '';
- /** Color of the icon. */
- @Input()
- get color(): string { return this._color; }
- set color(value: string) { this._updateColor(value); }
-
private _previousFontSetClass: string;
private _previousFontIconClass: string;
private _previousAriaLabel: string;
constructor(
- private _elementRef: ElementRef,
- private _renderer: Renderer,
- private _mdIconRegistry: MdIconRegistry) { }
-
- _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
- }
+ private _mdIconRegistry: MdIconRegistry,
+ elementRef: ElementRef,
+ renderer: Renderer) {
+ super(renderer, elementRef);
}
/**
diff --git a/src/lib/progress-spinner/progress-spinner.ts b/src/lib/progress-spinner/progress-spinner.ts
index 6cf9d76995df..5eb6855673de 100644
--- a/src/lib/progress-spinner/progress-spinner.ts
+++ b/src/lib/progress-spinner/progress-spinner.ts
@@ -8,6 +8,7 @@ import {
NgZone,
Renderer, Directive
} from '@angular/core';
+import {MdThemeable} from '../core/style/themeable';
// TODO(josephperrott): Benchpress tests.
@@ -72,7 +73,7 @@ export class MdProgressCircleCssMatStyler {}
styleUrls: ['progress-spinner.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MdProgressSpinner implements OnDestroy {
+export class MdProgressSpinner extends MdThemeable implements OnDestroy {
/** The id of the last requested animation. */
private _lastAnimationId: number = 0;
@@ -84,7 +85,6 @@ export class MdProgressSpinner implements OnDestroy {
private _mode: ProgressSpinnerMode = 'determinate';
private _value: number;
- private _color: string = 'primary';
/**
* Values for aria max and min are only defined as numbers when in a determinate mode. We do this
@@ -116,13 +116,6 @@ export class MdProgressSpinner implements OnDestroy {
this._cleanupIndeterminateAnimation();
}
- /** The color of the progress-spinner. Can be primary, accent, or warn. */
- @Input()
- get color(): string { return this._color; }
- set color(value: string) {
- this._updateColor(value);
- }
-
/** Value of the progress circle. It is bound to the host as the attribute aria-valuenow. */
@Input()
@HostBinding('attr.aria-valuenow')
@@ -159,11 +152,9 @@ export class MdProgressSpinner implements OnDestroy {
this._mode = m;
}
- constructor(
- private _ngZone: NgZone,
- private _elementRef: ElementRef,
- private _renderer: Renderer
- ) {}
+ constructor(private _ngZone: NgZone, elementRef: ElementRef, renderer: Renderer) {
+ super(renderer, elementRef);
+ }
/**
@@ -257,22 +248,6 @@ export class MdProgressSpinner implements OnDestroy {
}
}
- /**
- * Updates the color of the progress-spinner by adding the new palette class to the element
- * and removing the old one.
- */
- private _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- /** Sets the given palette class on the component element. */
- private _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
- }
- }
}
diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts
index bc96c9ca0f8f..45b245e54875 100644
--- a/src/lib/slide-toggle/slide-toggle.ts
+++ b/src/lib/slide-toggle/slide-toggle.ts
@@ -14,6 +14,7 @@ import {
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {applyCssTransform, coerceBooleanProperty, HammerInput} from '../core';
import {Observable} from 'rxjs/Observable';
+import {MdThemeable} from '../core/style/themeable';
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
@@ -52,7 +53,7 @@ let nextId = 0;
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
+export class MdSlideToggle extends MdThemeable implements AfterContentInit, ControlValueAccessor {
private onChange = (_: any) => {};
private onTouched = () => {};
@@ -60,7 +61,6 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
// A unique id for the slide-toggle. By default the id is auto-generated.
private _uniqueId = `md-slide-toggle-${++nextId}`;
private _checked: boolean = false;
- private _color: string;
private _isMousedown: boolean = false;
private _slideRenderer: SlideToggleRenderer = null;
private _disabled: boolean = false;
@@ -112,7 +112,9 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
@ViewChild('input') _inputElement: ElementRef;
- constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
+ constructor(elementRef: ElementRef, renderer: Renderer) {
+ super(renderer, elementRef);
+ }
ngAfterContentInit() {
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
@@ -211,30 +213,11 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
}
}
- /** The color of the slide-toggle. Can be primary, accent, or warn. */
- @Input()
- get color(): string { return this._color; }
- set color(value: string) {
- this._updateColor(value);
- }
-
/** Toggles the checked state of the slide-toggle. */
toggle() {
this.checked = !this.checked;
}
- private _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- private _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this._renderer.setElementClass(this._elementRef.nativeElement, `mat-${color}`, isAdd);
- }
- }
-
/** Emits the change event to the `change` output EventEmitter */
private _emitChangeEvent() {
let event = new MdSlideToggleChange();
diff --git a/src/lib/toolbar/toolbar.ts b/src/lib/toolbar/toolbar.ts
index 49f6620437fb..742f9b3646bd 100644
--- a/src/lib/toolbar/toolbar.ts
+++ b/src/lib/toolbar/toolbar.ts
@@ -1,12 +1,12 @@
import {
Component,
ChangeDetectionStrategy,
- Input,
ViewEncapsulation,
Directive,
ElementRef,
Renderer
} from '@angular/core';
+import {MdThemeable} from '../core/style/themeable';
@Directive({
@@ -29,32 +29,10 @@ export class MdToolbarRow {}
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
-export class MdToolbar {
+export class MdToolbar extends MdThemeable {
- private _color: string;
-
- constructor(private elementRef: ElementRef, private renderer: Renderer) { }
-
- /** The color of the toolbar. Can be primary, accent, or warn. */
- @Input()
- get color(): string {
- return this._color;
- }
-
- set color(value: string) {
- this._updateColor(value);
- }
-
- private _updateColor(newColor: string) {
- this._setElementColor(this._color, false);
- this._setElementColor(newColor, true);
- this._color = newColor;
- }
-
- private _setElementColor(color: string, isAdd: boolean) {
- if (color != null && color != '') {
- this.renderer.setElementClass(this.elementRef.nativeElement, `mat-${color}`, isAdd);
- }
+ constructor(elementRef: ElementRef, renderer: Renderer) {
+ super(renderer, elementRef);
}
}