diff --git a/src/lib/slider/slider.spec.ts b/src/lib/slider/slider.spec.ts index acc7562aea6f..dfbdb129c82f 100644 --- a/src/lib/slider/slider.spec.ts +++ b/src/lib/slider/slider.spec.ts @@ -24,6 +24,7 @@ describe('MdSlider', () => { SliderWithThumbLabel, SliderWithOneWayBinding, SliderWithTwoWayBinding, + SliderWithChangeHandler, ], providers: [ {provide: HAMMER_GESTURE_CONFIG, useFactory: () => { @@ -73,33 +74,33 @@ describe('MdSlider', () => { it('should update the value on a click', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEvent(sliderTrackElement, 0.19); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.19); // The expected value is 19 from: percentage * difference of max and min. expect(sliderInstance.value).toBe(19); }); it('should update the value on a slide', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.89, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.89, gestureConfig); // The expected value is 89 from: percentage * difference of max and min. expect(sliderInstance.value).toBe(89); }); it('should set the value as min when sliding before the track', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, -1.33, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, -1.33, gestureConfig); expect(sliderInstance.value).toBe(0); }); it('should set the value as max when sliding past the track', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 1.75, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 1.75, gestureConfig); expect(sliderInstance.value).toBe(100); }); it('should update the track fill on click', () => { expect(trackFillDimensions.width).toBe(0); - dispatchClickEvent(sliderTrackElement, 0.39); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.39); trackFillDimensions = trackFillElement.getBoundingClientRect(); thumbDimensions = thumbElement.getBoundingClientRect(); @@ -115,7 +116,7 @@ describe('MdSlider', () => { expect(thumbDimensions.left).toBe(sliderDimensions.left); // 50% is used here because the click event that is dispatched truncates the position and so // a value had to be used that would not be truncated. - dispatchClickEvent(sliderTrackElement, 0.5); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.5); thumbDimensions = thumbElement.getBoundingClientRect(); // The thumb position should be at 50% of the slider's width + the offset of the slider. @@ -125,7 +126,7 @@ describe('MdSlider', () => { it('should update the track fill on slide', () => { expect(trackFillDimensions.width).toBe(0); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.86, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.86, gestureConfig); trackFillDimensions = trackFillElement.getBoundingClientRect(); thumbDimensions = thumbElement.getBoundingClientRect(); @@ -141,7 +142,7 @@ describe('MdSlider', () => { expect(thumbDimensions.left).toBe(sliderDimensions.left); // The slide event also truncates the position passed in, so 50% is used here as well to // ensure the ability to calculate the expected position. - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.5, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.5, gestureConfig); thumbDimensions = thumbElement.getBoundingClientRect(); expect(thumbDimensions.left).toBe(sliderDimensions.width * 0.5 + sliderDimensions.left); @@ -151,7 +152,7 @@ describe('MdSlider', () => { let containerElement = sliderNativeElement.querySelector('.md-slider-container'); expect(containerElement.classList).not.toContain('md-slider-active'); - dispatchClickEvent(sliderNativeElement, 0.23); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.23); fixture.detectChanges(); expect(containerElement.classList).toContain('md-slider-active'); @@ -160,7 +161,7 @@ describe('MdSlider', () => { it('should remove the md-slider-active class on blur', () => { let containerElement = sliderNativeElement.querySelector('.md-slider-container'); - dispatchClickEvent(sliderNativeElement, 0.95); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.95); fixture.detectChanges(); expect(containerElement.classList).toContain('md-slider-active'); @@ -176,12 +177,12 @@ describe('MdSlider', () => { let containerElement = sliderNativeElement.querySelector('.md-slider-container'); expect(containerElement.classList).not.toContain('md-slider-sliding'); - dispatchSlideStartEvent(sliderNativeElement, 0, gestureConfig); + dispatchSlideStartEvent(sliderTrackElement, sliderNativeElement, 0, gestureConfig); fixture.detectChanges(); expect(containerElement.classList).toContain('md-slider-sliding'); - dispatchSlideEndEvent(sliderNativeElement, 0.34, gestureConfig); + dispatchSlideEndEvent(sliderTrackElement, sliderNativeElement, 0.34, gestureConfig); fixture.detectChanges(); expect(containerElement.classList).not.toContain('md-slider-sliding'); @@ -192,6 +193,7 @@ describe('MdSlider', () => { let fixture: ComponentFixture; let sliderDebugElement: DebugElement; let sliderNativeElement: HTMLElement; + let sliderTrackElement: HTMLElement; let sliderInstance: MdSlider; beforeEach(() => { @@ -200,6 +202,7 @@ describe('MdSlider', () => { sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider)); sliderNativeElement = sliderDebugElement.nativeElement; + sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track'); sliderInstance = sliderDebugElement.componentInstance; }); @@ -209,13 +212,13 @@ describe('MdSlider', () => { it('should not change the value on click when disabled', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEvent(sliderNativeElement, 0.63); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.63); expect(sliderInstance.value).toBe(0); }); it('should not change the value on slide when disabled', () => { expect(sliderInstance.value).toBe(0); - dispatchSlideEvent(sliderNativeElement, sliderNativeElement, 0, 0.5, gestureConfig); + dispatchSlideEventSequence(sliderNativeElement, sliderNativeElement, 0, 0.5, gestureConfig); expect(sliderInstance.value).toBe(0); }); @@ -223,7 +226,7 @@ describe('MdSlider', () => { let containerElement = sliderNativeElement.querySelector('.md-slider-container'); expect(containerElement.classList).not.toContain('md-slider-active'); - dispatchClickEvent(sliderNativeElement, 0.43); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.43); fixture.detectChanges(); expect(containerElement.classList).not.toContain('md-slider-active'); @@ -233,7 +236,7 @@ describe('MdSlider', () => { let containerElement = sliderNativeElement.querySelector('.md-slider-container'); expect(containerElement.classList).not.toContain('md-slider-sliding'); - dispatchSlideStartEvent(sliderNativeElement, 0.46, gestureConfig); + dispatchSlideStartEvent(sliderTrackElement, sliderNativeElement, 0.46, gestureConfig); fixture.detectChanges(); expect(containerElement.classList).not.toContain('md-slider-sliding'); @@ -270,7 +273,7 @@ describe('MdSlider', () => { }); it('should set the correct value on click', () => { - dispatchClickEvent(sliderTrackElement, 0.09); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.09); // Computed by multiplying the difference between the min and the max by the percentage from // the click and adding that to the minimum. let value = Math.round(4 + (0.09 * (6 - 4))); @@ -278,7 +281,7 @@ describe('MdSlider', () => { }); it('should set the correct value on slide', () => { - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.62, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.62, gestureConfig); // Computed by multiplying the difference between the min and the max by the percentage from // the click and adding that to the minimum. let value = Math.round(4 + (0.62 * (6 - 4))); @@ -286,7 +289,7 @@ describe('MdSlider', () => { }); it('should snap the thumb and fill to the nearest value on click', () => { - dispatchClickEvent(sliderTrackElement, 0.68); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.68); fixture.detectChanges(); let trackFillDimensions = trackFillElement.getBoundingClientRect(); @@ -299,10 +302,7 @@ describe('MdSlider', () => { }); it('should snap the thumb and fill to the nearest value on slide', () => { - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.74, gestureConfig); - fixture.detectChanges(); - - dispatchSlideEndEvent(sliderNativeElement, 0.74, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.74, gestureConfig); fixture.detectChanges(); let trackFillDimensions = trackFillElement.getBoundingClientRect(); @@ -338,14 +338,14 @@ describe('MdSlider', () => { }); it('should set the correct value on click', () => { - dispatchClickEvent(sliderTrackElement, 0.92); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.92); // On a slider with default max and min the value should be approximately equal to the // percentage clicked. This should be the case regardless of what the original set value was. expect(sliderInstance.value).toBe(92); }); it('should set the correct value on slide', () => { - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.32, gestureConfig); expect(sliderInstance.value).toBe(32); }); }); @@ -376,14 +376,14 @@ describe('MdSlider', () => { it('should set the correct step value on click', () => { expect(sliderInstance.value).toBe(0); - dispatchClickEvent(sliderTrackElement, 0.13); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.13); fixture.detectChanges(); expect(sliderInstance.value).toBe(25); }); it('should snap the thumb and fill to a step on click', () => { - dispatchClickEvent(sliderNativeElement, 0.66); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.66); fixture.detectChanges(); let trackFillDimensions = trackFillElement.getBoundingClientRect(); @@ -396,17 +396,14 @@ describe('MdSlider', () => { }); it('should set the correct step value on slide', () => { - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.07, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.07, gestureConfig); fixture.detectChanges(); expect(sliderInstance.value).toBe(0); }); it('should snap the thumb and fill to a step on slide', () => { - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.88, gestureConfig); - fixture.detectChanges(); - - dispatchSlideEndEvent(sliderNativeElement, 0.88, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.88, gestureConfig); fixture.detectChanges(); let trackFillDimensions = trackFillElement.getBoundingClientRect(); @@ -523,7 +520,7 @@ describe('MdSlider', () => { it('should update the thumb label text on click', () => { expect(thumbLabelTextElement.textContent).toBe('0'); - dispatchClickEvent(sliderTrackElement, 0.13); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.13); fixture.detectChanges(); // The thumb label text is set to the slider's value. These should always be the same. @@ -533,7 +530,7 @@ describe('MdSlider', () => { it('should update the thumb label text on slide', () => { expect(thumbLabelTextElement.textContent).toBe('0'); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.56, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.56, gestureConfig); fixture.detectChanges(); // The thumb label text is set to the slider's value. These should always be the same. @@ -544,7 +541,7 @@ describe('MdSlider', () => { expect(sliderContainerElement.classList).not.toContain('md-slider-active'); expect(sliderContainerElement.classList).toContain('md-slider-thumb-label-showing'); - dispatchClickEvent(sliderNativeElement, 0.49); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.49); fixture.detectChanges(); // The thumb label appears when the slider is active and the 'md-slider-thumb-label-showing' @@ -556,7 +553,7 @@ describe('MdSlider', () => { it('should show the thumb label on slide', () => { expect(sliderContainerElement.classList).not.toContain('md-slider-active'); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.91, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.91, gestureConfig); fixture.detectChanges(); expect(sliderContainerElement.classList).toContain('md-slider-thumb-label-showing'); @@ -596,7 +593,7 @@ describe('MdSlider', () => { it('should update the control on click', () => { expect(testComponent.control.value).toBe(0); - dispatchClickEvent(sliderTrackElement, 0.76); + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.76); fixture.detectChanges(); expect(testComponent.control.value).toBe(76); @@ -605,7 +602,7 @@ describe('MdSlider', () => { it('should update the control on slide', () => { expect(testComponent.control.value).toBe(0); - dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.19, gestureConfig); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.19, gestureConfig); fixture.detectChanges(); expect(testComponent.control.value).toBe(19); @@ -670,6 +667,54 @@ describe('MdSlider', () => { expect(thumbPosition).toBe(sliderDimensions.width * 3 / 4); }); }); + + describe('slider with change handler', () => { + let fixture: ComponentFixture; + let sliderDebugElement: DebugElement; + let sliderNativeElement: HTMLElement; + let sliderTrackElement: HTMLElement; + let testComponent: SliderWithChangeHandler; + + beforeEach(() => { + fixture = TestBed.createComponent(SliderWithChangeHandler); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + spyOn(testComponent, 'onChange'); + + sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider)); + sliderNativeElement = sliderDebugElement.nativeElement; + sliderTrackElement = sliderNativeElement.querySelector('.md-slider-track'); + }); + + it('should emit change on click', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.2); + fixture.detectChanges(); + + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + + it('should emit change on slide', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0, 0.4, gestureConfig); + fixture.detectChanges(); + + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + + it('should not emit multiple changes for same value', () => { + expect(testComponent.onChange).not.toHaveBeenCalled(); + + dispatchClickEvent(sliderTrackElement, sliderNativeElement, 0.6); + dispatchSlideEventSequence(sliderTrackElement, sliderNativeElement, 0.6, 0.6, gestureConfig); + fixture.detectChanges(); + + expect(testComponent.onChange).toHaveBeenCalledTimes(1); + }); + }); }); // The transition has to be removed in order to test the updated positions without setTimeout. @@ -734,64 +779,81 @@ class SliderWithTwoWayBinding { control = new FormControl(''); } +@Component({ + template: `` +}) +class SliderWithChangeHandler { + onChange() { } +} + /** * Dispatches a click event from an element. * Note: The mouse event truncates the position for the click. - * @param element The element from which the event will be dispatched. + * @param trackElement The track element from which the event location will be calculated. + * @param containerElement The container element from which the event will be dispatched. * @param percentage The percentage of the slider where the click should occur. Used to find the * physical location of the click. */ -function dispatchClickEvent(element: HTMLElement, percentage: number): void { - let dimensions = element.getBoundingClientRect(); +function dispatchClickEvent(trackElement: HTMLElement, containerElement: HTMLElement, + percentage: number): void { + let dimensions = trackElement.getBoundingClientRect(); let y = dimensions.top; let x = dimensions.left + (dimensions.width * percentage); let event = document.createEvent('MouseEvent'); event.initMouseEvent( 'click', true, true, window, 0, x, y, x, y, false, false, false, false, 0, null); - element.dispatchEvent(event); + containerElement.dispatchEvent(event); } /** - * Dispatches a slide event from an element. + * Dispatches a slide event sequence (consisting of slidestart, slide, slideend) from an element. * @param trackElement The track element from which the event location will be calculated. * @param containerElement The container element from which the event will be dispatched. * @param startPercent The percentage of the slider where the slide will begin. * @param endPercent The percentage of the slider where the slide will end. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ +function dispatchSlideEventSequence(trackElement: HTMLElement, containerElement: HTMLElement, + startPercent: number, endPercent: number, + gestureConfig: TestGestureConfig): void { + dispatchSlideStartEvent(trackElement, containerElement, startPercent, gestureConfig); + dispatchSlideEvent(trackElement, containerElement, startPercent, gestureConfig); + dispatchSlideEvent(trackElement, containerElement, endPercent, gestureConfig); + dispatchSlideEndEvent(trackElement, containerElement, endPercent, gestureConfig); +} + +/** + * Dispatches a slide event from an element. + * @param trackElement The track element from which the event location will be calculated. + * @param containerElement The container element from which the event will be dispatched. + * @param percent The percentage of the slider where the slide will happen. + * @param gestureConfig The gesture config for the test to handle emitting the slide events. + */ function dispatchSlideEvent(trackElement: HTMLElement, containerElement: HTMLElement, - startPercent: number, endPercent: number, - gestureConfig: TestGestureConfig): void { + percent: number, gestureConfig: TestGestureConfig): void { let dimensions = trackElement.getBoundingClientRect(); - let startX = dimensions.left + (dimensions.width * startPercent); - let endX = dimensions.left + (dimensions.width * endPercent); - - gestureConfig.emitEventForElement('slidestart', containerElement, { - // The actual event has a center with an x value that the slide listener is looking for. - center: { x: startX }, - // The event needs a source event with a prevent default so we fake one. - srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } - }); + let x = dimensions.left + (dimensions.width * percent); gestureConfig.emitEventForElement('slide', containerElement, { - center: { x: endX }, + center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); } /** * Dispatches a slidestart event from an element. - * @param element The element from which the event will be dispatched. - * @param startPercent The percentage of the slider where the slide will begin. + * @param trackElement The track element from which the event location will be calculated. + * @param containerElement The container element from which the event will be dispatched. + * @param percent The percentage of the slider where the slide will begin. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideStartEvent(element: HTMLElement, startPercent: number, - gestureConfig: TestGestureConfig): void { - let dimensions = element.getBoundingClientRect(); - let x = dimensions.left + (dimensions.width * startPercent); +function dispatchSlideStartEvent(trackElement: HTMLElement, containerElement: HTMLElement, + percent: number, gestureConfig: TestGestureConfig): void { + let dimensions = trackElement.getBoundingClientRect(); + let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slidestart', element, { + gestureConfig.emitEventForElement('slidestart', containerElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); @@ -799,16 +861,17 @@ function dispatchSlideStartEvent(element: HTMLElement, startPercent: number, /** * Dispatches a slideend event from an element. - * @param element The element from which the event will be dispatched. - * @param endPercent The percentage of the slider where the slide will end. + * @param trackElement The track element from which the event location will be calculated. + * @param containerElement The container element from which the event will be dispatched. + * @param percent The percentage of the slider where the slide will end. * @param gestureConfig The gesture config for the test to handle emitting the slide events. */ -function dispatchSlideEndEvent(element: HTMLElement, endPercent: number, - gestureConfig: TestGestureConfig): void { - let dimensions = element.getBoundingClientRect(); - let x = dimensions.left + (dimensions.width * endPercent); +function dispatchSlideEndEvent(trackElement: HTMLElement, containerElement: HTMLElement, + percent: number, gestureConfig: TestGestureConfig): void { + let dimensions = trackElement.getBoundingClientRect(); + let x = dimensions.left + (dimensions.width * percent); - gestureConfig.emitEventForElement('slideend', element, { + gestureConfig.emitEventForElement('slideend', containerElement, { center: { x: x }, srcEvent: { preventDefault: jasmine.createSpy('preventDefault') } }); diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 455b8f83dfb6..fe9be3e1cdd9 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -3,8 +3,10 @@ import { ModuleWithProviders, Component, ElementRef, + EventEmitter, HostBinding, Input, + Output, ViewEncapsulation, AfterContentInit, forwardRef, @@ -30,6 +32,12 @@ export const MD_SLIDER_VALUE_ACCESSOR: any = { multi: true }; +/** A simple change event emitted by the MdSlider component. */ +export class MdSliderChange { + source: MdSlider; + value: number; +} + @Component({ moduleId: module.id, selector: 'md-slider', @@ -80,6 +88,9 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { private _controlValueAccessorChangeFn: (value: any) => void = (value) => {}; + /** The last value for which a change event was emitted. */ + private _lastEmittedValue: number = null; + /** onTouch function registered via registerOnTouch (ControlValueAccessor). */ onTouched: () => any = () => {}; @@ -157,6 +168,8 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { this.snapThumbToValue(); } + @Output() change = new EventEmitter(); + constructor(elementRef: ElementRef) { this._renderer = new SliderRenderer(elementRef); } @@ -186,6 +199,7 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { this._renderer.addFocus(); this.updateValueFromPosition(event.clientX); this.snapThumbToValue(); + this._emitValueIfChanged(); } /** TODO: internal */ @@ -216,6 +230,7 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { onSlideEnd() { this.isSliding = false; this.snapThumbToValue(); + this._emitValueIfChanged(); } /** TODO: internal */ @@ -271,6 +286,17 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { } } + /** Emits a change event if the current value is different from the last emitted value. */ + private _emitValueIfChanged() { + if (this.value != this._lastEmittedValue) { + let event = new MdSliderChange(); + event.source = this; + event.value = this.value; + this.change.emit(event); + this._lastEmittedValue = this.value; + } + } + /** * Calculates the separation in pixels of tick marks. If there is no tick interval or the interval * is set to something other than a number or 'auto', nothing happens. @@ -358,10 +384,6 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor { */ writeValue(value: any) { this.value = value; - - if (this._sliderDimensions) { - this.snapThumbToValue(); - } } /**