diff --git a/src/components/slider/slider.spec.ts b/src/components/slider/slider.spec.ts index d87c58877200..75a15eb8a349 100644 --- a/src/components/slider/slider.spec.ts +++ b/src/components/slider/slider.spec.ts @@ -6,6 +6,7 @@ import { ComponentFixture, TestBed, } from '@angular/core/testing'; +import {ReactiveFormsModule, FormControl} from '@angular/forms'; import {Component, DebugElement, ViewEncapsulation} from '@angular/core'; import {By} from '@angular/platform-browser'; import {MdSlider, MdSliderModule} from './slider'; @@ -18,7 +19,7 @@ describe('MdSlider', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdSliderModule], + imports: [MdSliderModule, ReactiveFormsModule], declarations: [ StandardSlider, DisabledSlider, @@ -28,6 +29,7 @@ describe('MdSlider', () => { SliderWithAutoTickInterval, SliderWithSetTickInterval, SliderWithThumbLabel, + SliderWithTwoWayBinding, ], }); @@ -588,6 +590,67 @@ describe('MdSlider', () => { expect(sliderContainerElement.classList).toContain('md-slider-active'); }); }); + + describe('slider as a custom form control', () => { + let fixture: ComponentFixture; + let sliderDebugElement: DebugElement; + let sliderNativeElement: HTMLElement; + let sliderInstance: MdSlider; + let sliderTrackElement: HTMLElement; + let testComponent: SliderWithTwoWayBinding; + + beforeEach(async(() => { + builder.createAsync(SliderWithTwoWayBinding).then(f => { + fixture = f; + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider)); + sliderNativeElement = sliderDebugElement.nativeElement; + sliderInstance = sliderDebugElement.injector.get(MdSlider); + sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track'); + }); + })); + + it('should update the control when the value is updated', () => { + expect(testComponent.control.value).toBe(0); + + sliderInstance.value = 11; + fixture.detectChanges(); + + expect(testComponent.control.value).toBe(11); + }); + + it('should update the control on click', () => { + expect(testComponent.control.value).toBe(0); + + dispatchClickEvent(sliderTrackElement, 0.76); + fixture.detectChanges(); + + expect(testComponent.control.value).toBe(76); + }); + + it('should update the control on slide', () => { + expect(testComponent.control.value).toBe(0); + + dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.19, gestureConfig); + fixture.detectChanges(); + + expect(testComponent.control.value).toBe(19); + }); + + it('should update the value when the control is set', () => { + expect(sliderInstance.value).toBe(0); + + testComponent.control.setValue(7); + fixture.detectChanges(); + + expect(sliderInstance.value).toBe(7); + }); + + // TODO: Add tests for ng-pristine, ng-touched, ng-invalid. + }); }); // The transition has to be removed in order to test the updated positions without setTimeout. @@ -655,6 +718,13 @@ class SliderWithSetTickInterval { } }) class SliderWithThumbLabel { } +@Component({ + template: `` +}) +class SliderWithTwoWayBinding { + control = new FormControl(''); +} + /** * Dispatches a click event from an element. * Note: The mouse event truncates the position for the click. diff --git a/src/components/slider/slider.ts b/src/components/slider/slider.ts index b6e18d9921af..8027f1584bb3 100644 --- a/src/components/slider/slider.ts +++ b/src/components/slider/slider.ts @@ -6,7 +6,13 @@ import { Input, ViewEncapsulation, AfterContentInit, + forwardRef, } from '@angular/core'; +import { + NG_VALUE_ACCESSOR, + ControlValueAccessor, + FormsModule, +} from '@angular/forms'; import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value'; import {applyCssTransform} from '@angular2-material/core/style/apply-transform'; @@ -18,9 +24,20 @@ import {MdGestureConfig} from '@angular2-material/core/core'; */ const MIN_AUTO_TICK_SEPARATION = 30; +/** + * Provider Expression that allows md-slider to register as a ControlValueAccessor. + * This allows it to support [(ngModel)] and [formControl]. + */ +export const MD_SLIDER_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MdSlider), + multi: true +}; + @Component({ moduleId: module.id, selector: 'md-slider', + providers: [MD_SLIDER_VALUE_ACCESSOR], host: { 'tabindex': '0', '(click)': 'onClick($event)', @@ -34,7 +51,7 @@ const MIN_AUTO_TICK_SEPARATION = 30; styleUrls: ['slider.css'], encapsulation: ViewEncapsulation.None, }) -export class MdSlider implements AfterContentInit { +export class MdSlider implements AfterContentInit, ControlValueAccessor { /** A renderer to handle updating the slider's thumb and fill track. */ private _renderer: SliderRenderer = null; @@ -61,6 +78,11 @@ export class MdSlider implements AfterContentInit { /** The percentage of the slider that coincides with the value. */ private _percent: number = 0; + private _controlValueAccessorChangeFn: (value: any) => void = (value) => {}; + + /** onTouch function registered via registerOnTouch (ControlValueAccessor). */ + onTouched: () => any = () => {}; + /** The values at which the thumb will snap. */ @Input() step: number = 1; @@ -123,8 +145,15 @@ export class MdSlider implements AfterContentInit { } set value(v: number) { + // Only set the value to a valid number. v is casted to an any as we know it will come in as a + // string but it is labeled as a number which causes parseFloat to not accept it. + if (isNaN(parseFloat( v))) { + return; + } + this._value = Number(v); this._isInitialized = true; + this._controlValueAccessorChangeFn(this._value); } constructor(elementRef: ElementRef) { @@ -138,7 +167,10 @@ export class MdSlider implements AfterContentInit { */ ngAfterContentInit() { this._sliderDimensions = this._renderer.getSliderDimensions(); - this.snapToValue(); + // This needs to be called after content init because the value can be set to the min if the + // value itself isn't set. If this happens, the control value accessor needs to be updated. + this._controlValueAccessorChangeFn(this.value); + this.snapThumbToValue(); this._updateTickSeparation(); } @@ -152,7 +184,7 @@ export class MdSlider implements AfterContentInit { this.isSliding = false; this._renderer.addFocus(); this.updateValueFromPosition(event.clientX); - this.snapToValue(); + this.snapThumbToValue(); } /** TODO: internal */ @@ -182,7 +214,7 @@ export class MdSlider implements AfterContentInit { /** TODO: internal */ onSlideEnd() { this.isSliding = false; - this.snapToValue(); + this.snapThumbToValue(); } /** TODO: internal */ @@ -196,6 +228,7 @@ export class MdSlider implements AfterContentInit { /** TODO: internal */ onBlur() { this.isActive = false; + this.onTouched(); } /** @@ -230,7 +263,7 @@ export class MdSlider implements AfterContentInit { * Snaps the thumb to the current value. * Called after a click or drag event is over. */ - snapToValue() { + snapThumbToValue() { this.updatePercentFromValue(); this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width); } @@ -315,6 +348,34 @@ export class MdSlider implements AfterContentInit { clamp(value: number, min = 0, max = 1) { return Math.max(min, Math.min(value, max)); } + + /** + * Implemented as part of ControlValueAccessor. + * TODO: internal + */ + writeValue(value: any) { + this.value = value; + + if (this._sliderDimensions) { + this.snapThumbToValue(); + } + } + + /** + * Implemented as part of ControlValueAccessor. + * TODO: internal + */ + registerOnChange(fn: (value: any) => void) { + this._controlValueAccessorChangeFn = fn; + } + + /** + * Implemented as part of ControlValueAccessor. + * TODO: internal + */ + registerOnTouched(fn: any) { + this.onTouched = fn; + } } /** @@ -392,6 +453,7 @@ export const MD_SLIDER_DIRECTIVES = [MdSlider]; @NgModule({ + imports: [FormsModule], exports: MD_SLIDER_DIRECTIVES, declarations: MD_SLIDER_DIRECTIVES, providers: [ diff --git a/src/demo-app/slider/slider-demo.html b/src/demo-app/slider/slider-demo.html index a03d64082a38..caa923afb276 100644 --- a/src/demo-app/slider/slider-demo.html +++ b/src/demo-app/slider/slider-demo.html @@ -1,31 +1,21 @@

Default Slider

-
- Label - {{slidey.value}} -
+Label +{{slidey.value}}

Slider with Min and Max

-
- - {{slider2.value}} -
+ +{{slider2.value}}

Disabled Slider

-
- - {{slider3.value}} -
+ +{{slider3.value}}

Slider with set value

-
- -
+

Slider with step defined

-
- - {{slider5.value}} -
+ +{{slider5.value}}

Slider with set tick interval

@@ -33,3 +23,7 @@

Slider with set tick interval

Slider with Thumb Label

+ +

Slider with two-way binding

+ +