Skip to content

Commit 8828358

Browse files
iveysaurjelbourn
authored andcommitted
feat(slider): support ngModel (#1029)
1 parent fa67dee commit 8828358

File tree

3 files changed

+151
-25
lines changed

3 files changed

+151
-25
lines changed

src/components/slider/slider.spec.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ComponentFixture,
77
TestBed,
88
} from '@angular/core/testing';
9+
import {ReactiveFormsModule, FormControl} from '@angular/forms';
910
import {Component, DebugElement, ViewEncapsulation} from '@angular/core';
1011
import {By} from '@angular/platform-browser';
1112
import {MdSlider, MdSliderModule} from './slider';
@@ -18,7 +19,7 @@ describe('MdSlider', () => {
1819

1920
beforeEach(async(() => {
2021
TestBed.configureTestingModule({
21-
imports: [MdSliderModule],
22+
imports: [MdSliderModule, ReactiveFormsModule],
2223
declarations: [
2324
StandardSlider,
2425
DisabledSlider,
@@ -28,6 +29,7 @@ describe('MdSlider', () => {
2829
SliderWithAutoTickInterval,
2930
SliderWithSetTickInterval,
3031
SliderWithThumbLabel,
32+
SliderWithTwoWayBinding,
3133
],
3234
});
3335

@@ -588,6 +590,67 @@ describe('MdSlider', () => {
588590
expect(sliderContainerElement.classList).toContain('md-slider-active');
589591
});
590592
});
593+
594+
describe('slider as a custom form control', () => {
595+
let fixture: ComponentFixture<SliderWithTwoWayBinding>;
596+
let sliderDebugElement: DebugElement;
597+
let sliderNativeElement: HTMLElement;
598+
let sliderInstance: MdSlider;
599+
let sliderTrackElement: HTMLElement;
600+
let testComponent: SliderWithTwoWayBinding;
601+
602+
beforeEach(async(() => {
603+
builder.createAsync(SliderWithTwoWayBinding).then(f => {
604+
fixture = f;
605+
fixture.detectChanges();
606+
607+
testComponent = fixture.debugElement.componentInstance;
608+
609+
sliderDebugElement = fixture.debugElement.query(By.directive(MdSlider));
610+
sliderNativeElement = sliderDebugElement.nativeElement;
611+
sliderInstance = sliderDebugElement.injector.get(MdSlider);
612+
sliderTrackElement = <HTMLElement>sliderNativeElement.querySelector('.md-slider-track');
613+
});
614+
}));
615+
616+
it('should update the control when the value is updated', () => {
617+
expect(testComponent.control.value).toBe(0);
618+
619+
sliderInstance.value = 11;
620+
fixture.detectChanges();
621+
622+
expect(testComponent.control.value).toBe(11);
623+
});
624+
625+
it('should update the control on click', () => {
626+
expect(testComponent.control.value).toBe(0);
627+
628+
dispatchClickEvent(sliderTrackElement, 0.76);
629+
fixture.detectChanges();
630+
631+
expect(testComponent.control.value).toBe(76);
632+
});
633+
634+
it('should update the control on slide', () => {
635+
expect(testComponent.control.value).toBe(0);
636+
637+
dispatchSlideEvent(sliderTrackElement, sliderNativeElement, 0, 0.19, gestureConfig);
638+
fixture.detectChanges();
639+
640+
expect(testComponent.control.value).toBe(19);
641+
});
642+
643+
it('should update the value when the control is set', () => {
644+
expect(sliderInstance.value).toBe(0);
645+
646+
testComponent.control.setValue(7);
647+
fixture.detectChanges();
648+
649+
expect(sliderInstance.value).toBe(7);
650+
});
651+
652+
// TODO: Add tests for ng-pristine, ng-touched, ng-invalid.
653+
});
591654
});
592655

593656
// The transition has to be removed in order to test the updated positions without setTimeout.
@@ -655,6 +718,13 @@ class SliderWithSetTickInterval { }
655718
})
656719
class SliderWithThumbLabel { }
657720

721+
@Component({
722+
template: `<md-slider [formControl]="control"></md-slider>`
723+
})
724+
class SliderWithTwoWayBinding {
725+
control = new FormControl('');
726+
}
727+
658728
/**
659729
* Dispatches a click event from an element.
660730
* Note: The mouse event truncates the position for the click.

src/components/slider/slider.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
Input,
77
ViewEncapsulation,
88
AfterContentInit,
9+
forwardRef,
910
} from '@angular/core';
11+
import {
12+
NG_VALUE_ACCESSOR,
13+
ControlValueAccessor,
14+
FormsModule,
15+
} from '@angular/forms';
1016
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
1117
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
1218
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
@@ -18,9 +24,20 @@ import {MdGestureConfig} from '@angular2-material/core/core';
1824
*/
1925
const MIN_AUTO_TICK_SEPARATION = 30;
2026

27+
/**
28+
* Provider Expression that allows md-slider to register as a ControlValueAccessor.
29+
* This allows it to support [(ngModel)] and [formControl].
30+
*/
31+
export const MD_SLIDER_VALUE_ACCESSOR: any = {
32+
provide: NG_VALUE_ACCESSOR,
33+
useExisting: forwardRef(() => MdSlider),
34+
multi: true
35+
};
36+
2137
@Component({
2238
moduleId: module.id,
2339
selector: 'md-slider',
40+
providers: [MD_SLIDER_VALUE_ACCESSOR],
2441
host: {
2542
'tabindex': '0',
2643
'(click)': 'onClick($event)',
@@ -34,7 +51,7 @@ const MIN_AUTO_TICK_SEPARATION = 30;
3451
styleUrls: ['slider.css'],
3552
encapsulation: ViewEncapsulation.None,
3653
})
37-
export class MdSlider implements AfterContentInit {
54+
export class MdSlider implements AfterContentInit, ControlValueAccessor {
3855
/** A renderer to handle updating the slider's thumb and fill track. */
3956
private _renderer: SliderRenderer = null;
4057

@@ -61,6 +78,11 @@ export class MdSlider implements AfterContentInit {
6178
/** The percentage of the slider that coincides with the value. */
6279
private _percent: number = 0;
6380

81+
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
82+
83+
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
84+
onTouched: () => any = () => {};
85+
6486
/** The values at which the thumb will snap. */
6587
@Input() step: number = 1;
6688

@@ -123,8 +145,15 @@ export class MdSlider implements AfterContentInit {
123145
}
124146

125147
set value(v: number) {
148+
// Only set the value to a valid number. v is casted to an any as we know it will come in as a
149+
// string but it is labeled as a number which causes parseFloat to not accept it.
150+
if (isNaN(parseFloat(<any> v))) {
151+
return;
152+
}
153+
126154
this._value = Number(v);
127155
this._isInitialized = true;
156+
this._controlValueAccessorChangeFn(this._value);
128157
}
129158

130159
constructor(elementRef: ElementRef) {
@@ -138,7 +167,10 @@ export class MdSlider implements AfterContentInit {
138167
*/
139168
ngAfterContentInit() {
140169
this._sliderDimensions = this._renderer.getSliderDimensions();
141-
this.snapToValue();
170+
// This needs to be called after content init because the value can be set to the min if the
171+
// value itself isn't set. If this happens, the control value accessor needs to be updated.
172+
this._controlValueAccessorChangeFn(this.value);
173+
this.snapThumbToValue();
142174
this._updateTickSeparation();
143175
}
144176

@@ -152,7 +184,7 @@ export class MdSlider implements AfterContentInit {
152184
this.isSliding = false;
153185
this._renderer.addFocus();
154186
this.updateValueFromPosition(event.clientX);
155-
this.snapToValue();
187+
this.snapThumbToValue();
156188
}
157189

158190
/** TODO: internal */
@@ -182,7 +214,7 @@ export class MdSlider implements AfterContentInit {
182214
/** TODO: internal */
183215
onSlideEnd() {
184216
this.isSliding = false;
185-
this.snapToValue();
217+
this.snapThumbToValue();
186218
}
187219

188220
/** TODO: internal */
@@ -196,6 +228,7 @@ export class MdSlider implements AfterContentInit {
196228
/** TODO: internal */
197229
onBlur() {
198230
this.isActive = false;
231+
this.onTouched();
199232
}
200233

201234
/**
@@ -230,7 +263,7 @@ export class MdSlider implements AfterContentInit {
230263
* Snaps the thumb to the current value.
231264
* Called after a click or drag event is over.
232265
*/
233-
snapToValue() {
266+
snapThumbToValue() {
234267
this.updatePercentFromValue();
235268
this._renderer.updateThumbAndFillPosition(this._percent, this._sliderDimensions.width);
236269
}
@@ -315,6 +348,34 @@ export class MdSlider implements AfterContentInit {
315348
clamp(value: number, min = 0, max = 1) {
316349
return Math.max(min, Math.min(value, max));
317350
}
351+
352+
/**
353+
* Implemented as part of ControlValueAccessor.
354+
* TODO: internal
355+
*/
356+
writeValue(value: any) {
357+
this.value = value;
358+
359+
if (this._sliderDimensions) {
360+
this.snapThumbToValue();
361+
}
362+
}
363+
364+
/**
365+
* Implemented as part of ControlValueAccessor.
366+
* TODO: internal
367+
*/
368+
registerOnChange(fn: (value: any) => void) {
369+
this._controlValueAccessorChangeFn = fn;
370+
}
371+
372+
/**
373+
* Implemented as part of ControlValueAccessor.
374+
* TODO: internal
375+
*/
376+
registerOnTouched(fn: any) {
377+
this.onTouched = fn;
378+
}
318379
}
319380

320381
/**
@@ -392,6 +453,7 @@ export const MD_SLIDER_DIRECTIVES = [MdSlider];
392453

393454

394455
@NgModule({
456+
imports: [FormsModule],
395457
exports: MD_SLIDER_DIRECTIVES,
396458
declarations: MD_SLIDER_DIRECTIVES,
397459
providers: [

src/demo-app/slider/slider-demo.html

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
<h1>Default Slider</h1>
2-
<section class="demo-section">
3-
Label <md-slider #slidey></md-slider>
4-
{{slidey.value}}
5-
</section>
2+
Label <md-slider #slidey></md-slider>
3+
{{slidey.value}}
64

75
<h1>Slider with Min and Max</h1>
8-
<section class="demo-section">
9-
<md-slider min="5" max="7" #slider2></md-slider>
10-
{{slider2.value}}
11-
</section>
6+
<md-slider min="5" max="7" #slider2></md-slider>
7+
{{slider2.value}}
128

139
<h1>Disabled Slider</h1>
14-
<section class="demo-section">
15-
<md-slider disabled #slider3></md-slider>
16-
{{slider3.value}}
17-
</section>
10+
<md-slider disabled #slider3></md-slider>
11+
{{slider3.value}}
1812

1913
<h1>Slider with set value</h1>
20-
<section class="demo-section">
21-
<md-slider value="43" #slider4></md-slider>
22-
</section>
14+
<md-slider value="43"></md-slider>
2315

2416
<h1>Slider with step defined</h1>
25-
<section class="demo-section">
26-
<md-slider min="1" max="100" step="20" #slider5></md-slider>
27-
{{slider5.value}}
28-
</section>
17+
<md-slider min="1" max="100" step="20" #slider5></md-slider>
18+
{{slider5.value}}
2919

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

3424
<h1>Slider with Thumb Label</h1>
3525
<md-slider thumb-label></md-slider>
26+
27+
<h1>Slider with two-way binding</h1>
28+
<md-slider [(ngModel)]="demo" step="40"></md-slider>
29+
<input [(ngModel)]="demo">

0 commit comments

Comments
 (0)