feat(slider): support ngModel (#1029)

This commit is contained in:
Ivey Padgett
2016-08-12 17:16:46 -07:00
committed by Jeremy Elbourn
parent fa67deec01
commit 8828358511
3 changed files with 151 additions and 25 deletions
+71 -1
View File
@@ -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<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.
@@ -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.
+67 -5
View File
@@ -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(<any> 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: [
+13 -19
View File
@@ -1,31 +1,21 @@
<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>
@@ -33,3 +23,7 @@
<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">