Skip to content

Commit

Permalink
feat(slider): support ngModel (#1029)
Browse files Browse the repository at this point in the history
  • Loading branch information
iveysaur authored and jelbourn committed Aug 13, 2016
1 parent fa67dee commit 8828358
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 25 deletions.
72 changes: 71 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,67 @@ describe('MdSlider', () => {
expect(sliderContainerElement.classList).toContain('md-slider-active');
});
});

describe('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);
});

// 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.
Expand Down Expand Up @@ -655,6 +718,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
72 changes: 67 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';
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,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();
}

Expand All @@ -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 */
Expand Down Expand Up @@ -182,7 +214,7 @@ export class MdSlider implements AfterContentInit {
/** TODO: internal */
onSlideEnd() {
this.isSliding = false;
this.snapToValue();
this.snapThumbToValue();
}

/** TODO: internal */
Expand All @@ -196,6 +228,7 @@ export class MdSlider implements AfterContentInit {
/** TODO: internal */
onBlur() {
this.isActive = false;
this.onTouched();
}

/**
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
}

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


@NgModule({
imports: [FormsModule],
exports: MD_SLIDER_DIRECTIVES,
declarations: MD_SLIDER_DIRECTIVES,
providers: [
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">

0 comments on commit 8828358

Please sign in to comment.