Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add two way binding to slider #1029

Merged
merged 4 commits into from
Aug 13, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
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';
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,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);
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 +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;
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 +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">