Skip to content

Commit

Permalink
refactor: add themeable base class
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
devversion committed Mar 20, 2017
1 parent 3ad6ff0 commit 705519d
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 192 deletions.
2 changes: 1 addition & 1 deletion src/demo-app/dialog/dialog-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class JazzDialog {
<button
md-button
color="secondary"
color="accent"
(click)="showInStackedDialog()">
Show in Dialog</button>
</md-dialog-actions>
Expand Down
30 changes: 7 additions & 23 deletions src/lib/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 => {
Expand All @@ -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');
Expand Down Expand Up @@ -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 */
Expand Down
35 changes: 11 additions & 24 deletions src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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. */
Expand All @@ -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';
}

Expand Down Expand Up @@ -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;
}
Expand Down
36 changes: 8 additions & 28 deletions src/lib/chips/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,17 +36,14 @@ 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;

/** 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<MdChipEvent>();

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

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 {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}
}
}
21 changes: 4 additions & 17 deletions src/lib/core/selection/pseudo-checkbox/pseudo-checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ElementRef,
Renderer,
} from '@angular/core';
import {MdThemeable} from '../../style/themeable';

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

Expand Down Expand Up @@ -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';
}
}
79 changes: 79 additions & 0 deletions src/lib/core/style/themeable.spec.ts
Original file line number Diff line number Diff line change
@@ -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<TestComponent>;
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: '<span>Themeable</span>'
})
class ThemeableComponent extends MdThemeable {
constructor(renderer: Renderer, elementRef: ElementRef) {
super(renderer, elementRef);
}
}

@Component({
template: '<themeable-test [color]="color"></themeable-test>'
})
class TestComponent {
color: string = 'warn';
}
Loading

0 comments on commit 705519d

Please sign in to comment.