Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion src/components/slider/slider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,7 +19,7 @@ describe('MdSlider', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdSliderModule],
imports: [MdSliderModule, ReactiveFormsModule],
declarations: [
StandardSlider,
DisabledSlider,
Expand All @@ -28,6 +29,7 @@ describe('MdSlider', () => {
SliderWithAutoTickInterval,
SliderWithSetTickInterval,
SliderWithThumbLabel,
SliderWithTwoWayBinding,
],
});

Expand Down Expand Up @@ -588,6 +590,65 @@ describe('MdSlider', () => {
expect(sliderContainerElement.classList).toContain('md-slider-active');
});
});

describe('slider with two-way binding', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got a one-way binding in this test :) Pet peeve, but can you change to read "slider as a custom form control"?

let fixture: ComponentFixture<SliderWithTwoWayBinding>;
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 = <HTMLElement>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);
})
});
});

// The transition has to be removed in order to test the updated positions without setTimeout.
Expand Down Expand Up @@ -655,6 +716,13 @@ class SliderWithSetTickInterval { }
})
class SliderWithThumbLabel { }

@Component({
template: `<md-slider [formControl]="control"></md-slider>`
})
class SliderWithTwoWayBinding {
control = new FormControl('');
}

/**
* Dispatches a click event from an element.
* Note: The mouse event truncates the position for the click.
Expand Down
69 changes: 64 additions & 5 deletions src/components/slider/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {
Input,
ViewEncapsulation,
AfterContentInit,
forwardRef,
} from '@angular/core';
import {
NG_VALUE_ACCESSOR,
ControlValueAccessor,
FormsModule,
} from '@angular/forms';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make import indent consistent in this file

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';
Expand All @@ -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)',
Expand All @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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(<any> v))) {
return;
}

this._value = Number(v);
this._isInitialized = true;
this._controlValueAccessorChangeFn(this._value);
}

constructor(elementRef: ElementRef) {
Expand All @@ -138,7 +167,8 @@ export class MdSlider implements AfterContentInit {
*/
ngAfterContentInit() {
this._sliderDimensions = this._renderer.getSliderDimensions();
this.snapToValue();
this._controlValueAccessorChangeFn(this.value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment explaining why this is necessary

this.snapThumbToValue();
this._updateTickSeparation();
}

Expand All @@ -152,7 +182,7 @@ export class MdSlider implements AfterContentInit {
this.isSliding = false;
this._renderer.addFocus();
this.updateValueFromPosition(event.clientX);
this.snapToValue();
this.snapThumbToValue();
}

/** TODO: internal */
Expand Down Expand Up @@ -182,7 +212,7 @@ export class MdSlider implements AfterContentInit {
/** TODO: internal */
onSlideEnd() {
this.isSliding = false;
this.snapToValue();
this.snapThumbToValue();
}

/** TODO: internal */
Expand Down Expand Up @@ -230,7 +260,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);
}
Expand Down Expand Up @@ -315,6 +345,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are you calling this? It doesn't look like the onBlur function calls it. Have you checked that the control is correctly set as touched?

}
}

/**
Expand Down Expand Up @@ -392,6 +450,7 @@ export const MD_SLIDER_DIRECTIVES = [MdSlider];


@NgModule({
imports: [FormsModule],
exports: MD_SLIDER_DIRECTIVES,
declarations: MD_SLIDER_DIRECTIVES,
providers: [
Expand Down
3 changes: 2 additions & 1 deletion src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {NgModule, ApplicationRef} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {HttpModule} from '@angular/http';
import {FormsModule} from '@angular/forms';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {DemoApp, Home} from './demo-app/demo-app';
import {DEMO_APP_ROUTE_PROVIDER} from './demo-app/routes';
import {RouterModule} from '@angular/router';
Expand Down Expand Up @@ -37,6 +37,7 @@ import {TabsDemo} from './tabs/tab-group-demo';
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
Copy link
Contributor

@kara kara Aug 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that in the demo you ended up with ngModel, you can remove ReactiveFormsModule from the demo module.

HttpModule,
MaterialModule,
RouterModule,
Expand Down
32 changes: 13 additions & 19 deletions src/demo-app/slider/slider-demo.html
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
<h1>Default Slider</h1>
<section class="demo-section">
Label <md-slider #slidey></md-slider>
{{slidey.value}}
</section>
Label <md-slider #slidey></md-slider>
{{slidey.value}}

<h1>Slider with Min and Max</h1>
<section class="demo-section">
<md-slider min="5" max="7" #slider2></md-slider>
{{slider2.value}}
</section>
<md-slider min="5" max="7" #slider2></md-slider>
{{slider2.value}}

<h1>Disabled Slider</h1>
<section class="demo-section">
<md-slider disabled #slider3></md-slider>
{{slider3.value}}
</section>
<md-slider disabled #slider3></md-slider>
{{slider3.value}}

<h1>Slider with set value</h1>
<section class="demo-section">
<md-slider value="43" #slider4></md-slider>
</section>
<md-slider value="43"></md-slider>

<h1>Slider with step defined</h1>
<section class="demo-section">
<md-slider min="1" max="100" step="20" #slider5></md-slider>
{{slider5.value}}
</section>
<md-slider min="1" max="100" step="20" #slider5></md-slider>
{{slider5.value}}

<h1>Slider with set tick interval</h1>
<md-slider tick-interval="auto"></md-slider>
<md-slider tick-interval="9"></md-slider>

<h1>Slider with Thumb Label</h1>
<md-slider thumb-label></md-slider>

<h1>Slider with two-way binding</h1>
<md-slider [(ngModel)]="demo" step="40"></md-slider>
<input [(ngModel)]="demo">