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); } }