Skip to content

Commit 48c8f40

Browse files
authored
feat(material-experimental/mdc-slider): implement MatSlider (angular#21680)
* feat(material-experimental/mdc-slider): implement MatSlider
1 parent 25193ca commit 48c8f40

File tree

5 files changed

+273
-16
lines changed

5 files changed

+273
-16
lines changed

src/material-experimental/mdc-slider/module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {MatSliderThumb} from './slider-thumb';
1414

1515
@NgModule({
1616
imports: [MatCommonModule, CommonModule],
17-
exports: [MatSlider],
17+
exports: [MatSlider, MatSliderThumb],
1818
declarations: [
1919
MatSlider,
2020
MatSliderThumb,

src/material-experimental/mdc-slider/slider-thumb.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import {NumberInput} from '@angular/cdk/coercion';
1010
import {DOCUMENT} from '@angular/common';
1111
import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core';
1212
import {Thumb} from '@material/slider';
13+
import {MatSlider} from './slider';
14+
15+
/**
16+
* Represents a drag event emitted by the MatSlider component.
17+
*/
18+
export interface MatSliderDragEvent {
19+
/** The MatSliderThumb that was interacted with. */
20+
source: MatSliderThumb;
21+
22+
/** The parent MatSlider that was interacted with. */
23+
parent: MatSlider;
24+
25+
/** The current value of the slider. */
26+
value: number;
27+
28+
/** The thumb that was interacted with. */
29+
thumb: Thumb;
30+
}
1331

1432
/**
1533
* The native input used by the MatSlider.
@@ -52,6 +70,14 @@ import {Thumb} from '@material/slider';
5270
@Input()
5371
set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); }
5472

73+
/** Event emitted when the slider thumb starts being dragged. */
74+
@Output() readonly dragStart: EventEmitter<MatSliderDragEvent>
75+
= new EventEmitter<MatSliderDragEvent>();
76+
77+
/** Event emitted when the slider thumb stops being dragged. */
78+
@Output() readonly dragEnd: EventEmitter<MatSliderDragEvent>
79+
= new EventEmitter<MatSliderDragEvent>();
80+
5581
/** Event emitted every time the MatSliderThumb is blurred. */
5682
@Output() readonly _blur: EventEmitter<void> = new EventEmitter<void>();
5783

@@ -63,17 +89,12 @@ import {Thumb} from '@material/slider';
6389

6490
constructor(
6591
@Inject(DOCUMENT) private readonly _document: Document,
66-
private readonly _elementRef: ElementRef<HTMLInputElement>,
92+
readonly _elementRef: ElementRef<HTMLInputElement>,
6793
) {}
6894

69-
/** Returns the hosts native HTML element. */
70-
_getHostElement(): HTMLInputElement {
71-
return this._elementRef.nativeElement;
72-
}
73-
7495
/** Returns true if this slider input currently has focus. */
7596
_isFocused(): boolean {
76-
return this._document.activeElement === this._getHostElement();
97+
return this._document.activeElement === this._elementRef.nativeElement;
7798
}
7899

79100
static ngAcceptInputType_value: NumberInput;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!-- Inputs -->
2+
<ng-content></ng-content>
3+
4+
<!-- Track -->
5+
<div class="mdc-slider__track">
6+
<div class="mdc-slider__track--inactive"></div>
7+
<div class="mdc-slider__track--active">
8+
<div class="mdc-slider__track--active_fill" #trackActive></div>
9+
</div>
10+
<div *ngIf="showTickMarks" class="mdc-slider__tick-marks" #tickMarkContainer>
11+
<div *ngFor="let tickMark of _tickMarks" [class]="_getTickMarkClass(tickMark)"></div>
12+
</div>
13+
</div>
14+
15+
<!-- Thumbs -->
16+
<div class="mdc-slider__thumb" *ngFor="let thumb of _getThumbTypes()" #thumb>
17+
<div class="mdc-slider__value-indicator-container" *ngIf="discrete">
18+
<div class="mdc-slider__value-indicator">
19+
<span class="mdc-slider__value-indicator-text">
20+
{{_getValueIndicatorTextByThumb(thumb)}}
21+
</span>
22+
</div>
23+
</div>
24+
<div class="mdc-slider__thumb-knob" #knob></div>
25+
</div>

src/material-experimental/mdc-slider/slider.ts

Lines changed: 219 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,33 @@
77
*/
88

99
import {
10+
BooleanInput,
11+
coerceBooleanProperty,
12+
coerceNumberProperty,
13+
NumberInput
14+
} from '@angular/cdk/coercion';
15+
import {Platform} from '@angular/cdk/platform';
16+
import {DOCUMENT} from '@angular/common';
17+
import {
18+
AfterViewInit,
1019
ChangeDetectionStrategy,
20+
ChangeDetectorRef,
1121
Component,
22+
ContentChildren,
23+
ElementRef,
24+
EventEmitter,
25+
Inject,
26+
Input,
27+
OnDestroy,
28+
Output,
29+
QueryList,
30+
ViewChild,
31+
ViewChildren,
1232
ViewEncapsulation,
1333
} from '@angular/core';
14-
import {MDCSliderFoundation} from '@material/slider';
34+
import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider';
1535
import {SliderAdapter} from './slider-adapter';
36+
import {MatSliderThumb} from './slider-thumb';
1637

1738
/**
1839
* Allows users to select from a range of values by moving the slider thumb. It is similar in
@@ -22,13 +43,207 @@ import {SliderAdapter} from './slider-adapter';
2243
selector: 'mat-slider',
2344
templateUrl: 'slider.html',
2445
styleUrls: ['slider.css'],
46+
host: {
47+
'class': 'mat-mdc-slider mdc-slider',
48+
'[class.mdc-slider--range]': '_isRange()',
49+
'[class.mdc-slider--disabled]': 'disabled',
50+
'[class.mdc-slider--discrete]': 'discrete',
51+
'[class.mdc-slider--tick-marks]': 'showTickMarks',
52+
},
53+
exportAs: 'matSlider',
2554
changeDetection: ChangeDetectionStrategy.OnPush,
2655
encapsulation: ViewEncapsulation.None,
2756
})
28-
export class MatSlider {
57+
export class MatSlider implements AfterViewInit, OnDestroy {
58+
/** The slider thumb(s). */
59+
@ViewChildren('thumb') _thumbs: QueryList<ElementRef<HTMLElement>>;
2960

30-
/** Instance of the MDC slider foundation for this slider. */
61+
/** The slider thumb knob(s) */
62+
@ViewChildren('knob') _knobs: QueryList<ElementRef<HTMLElement>>;
63+
64+
/** The active section of the slider track. */
65+
@ViewChild('trackActive') _trackActive: ElementRef<HTMLElement>;
66+
67+
/** The sliders hidden range input(s). */
68+
@ContentChildren(MatSliderThumb, {descendants: false}) _inputs: QueryList<MatSliderThumb>;
69+
70+
/** Whether the slider is disabled. */
71+
@Input()
72+
get disabled(): boolean { return this._disabled; }
73+
set disabled(v: boolean) {
74+
this._disabled = coerceBooleanProperty(v);
75+
76+
// If we want to disable the slider after the foundation has been initialized,
77+
// we need to inform the foundation by calling `setDisabled`. Also, we can't call
78+
// this before initializing the foundation because it will throw errors.
79+
if (this._initialized) {
80+
this._foundation.setDisabled(v);
81+
}
82+
}
83+
private _disabled: boolean = false;
84+
85+
/** Whether the slider displays a numeric value label upon pressing the thumb. */
86+
@Input()
87+
get discrete(): boolean { return this._discrete; }
88+
set discrete(v: boolean) { this._discrete = coerceBooleanProperty(v); }
89+
private _discrete: boolean = false;
90+
91+
/** Whether the slider displays tick marks along the slider track. */
92+
@Input()
93+
get showTickMarks(): boolean { return this._showTickMarks; }
94+
set showTickMarks(v: boolean) {
95+
this._showTickMarks = coerceBooleanProperty(v);
96+
}
97+
private _showTickMarks: boolean = false;
98+
99+
/** The minimum value that the slider can have. */
100+
@Input()
101+
get min(): number { return this._min; }
102+
set min(v: number) { this._min = coerceNumberProperty(v, this._min); }
103+
private _min = 0;
31104

32-
// tslint:disable-next-line:no-unused-variable
105+
/** The maximum value that the slider can have. */
106+
@Input()
107+
get max(): number { return this._max; }
108+
set max(v: number) { this._max = coerceNumberProperty(v, this._max); }
109+
private _max = 100;
110+
111+
/** The values at which the thumb will snap. */
112+
@Input()
113+
get step(): number { return this._step; }
114+
set step(v: number) { this._step = coerceNumberProperty(v, this._step); }
115+
private _step: number = 1;
116+
117+
/**
118+
* Function that will be used to format the value before it is displayed
119+
* in the thumb label. Can be used to format very large number in order
120+
* for them to fit into the slider thumb.
121+
*/
122+
@Input() displayWith: ((value: number) => string) | null;
123+
124+
/** Instance of the MDC slider foundation for this slider. */
33125
private _foundation = new MDCSliderFoundation(new SliderAdapter());
126+
127+
/** Whether the foundation has been initialized. */
128+
_initialized: boolean = false;
129+
130+
/** The string representation of the start thumbs value. */
131+
_startValueIndicatorText: string;
132+
133+
/** The string representation of the end thumbs value. */
134+
_endValueIndicatorText: string;
135+
136+
/** The injected document if available or fallback to the global document reference. */
137+
_document: Document;
138+
139+
/**
140+
* The defaultView of the injected document if
141+
* available or fallback to global window reference.
142+
*/
143+
_window: Window;
144+
145+
/** The hosts native HTML element. */
146+
_hostElement: HTMLElement;
147+
148+
/** Used to keep track of & render the active & inactive tick marks on the slider track. */
149+
_tickMarks: TickMark[];
150+
151+
constructor(
152+
readonly _cdr: ChangeDetectorRef,
153+
private readonly _elementRef: ElementRef<HTMLElement>,
154+
private readonly _platform: Platform,
155+
@Inject(DOCUMENT) document: any) {
156+
this._document = document;
157+
this._window = this._document.defaultView || window;
158+
this._hostElement = this._elementRef.nativeElement;
159+
}
160+
161+
ngAfterViewInit() {
162+
this._foundation.init();
163+
if (this._platform.isBrowser) {
164+
this._foundation.layout();
165+
}
166+
this._initialized = true;
167+
}
168+
169+
ngOnDestroy() {
170+
if (this._platform.isBrowser) {
171+
this._foundation.destroy();
172+
}
173+
}
174+
175+
/** Gets the current value of given slider thumb. */
176+
_getValue(thumb: Thumb): number {
177+
return thumb === Thumb.START
178+
? this._foundation.getValueStart()
179+
: this._foundation.getValue();
180+
}
181+
182+
/** Sets the value of a slider thumb. */
183+
_setValue(value: number, thumb: Thumb): void {
184+
thumb === Thumb.START
185+
? this._foundation.setValueStart(value)
186+
: this._foundation.setValue(value);
187+
}
188+
189+
/** Whether this is a ranged slider. */
190+
_isRange(): boolean {
191+
return this._inputs.length === 2;
192+
}
193+
194+
/** Gets the slider thumb input of the given thumb. */
195+
_getInput(thumb: Thumb): MatSliderThumb {
196+
return thumb === Thumb.END ? this._inputs.get(this._inputs.length - 1)! : this._inputs.get(0)!;
197+
}
198+
199+
/** Gets the slider thumb HTML input element of the given thumb. */
200+
_getInputElement(thumb: Thumb): HTMLInputElement {
201+
return this._getInput(thumb)._elementRef.nativeElement;
202+
}
203+
204+
/** Gets the slider thumb HTML element of the given thumb. */
205+
_getThumbElement(thumb: Thumb): HTMLElement {
206+
const thumbs = this._thumbs.toArray().map(e => e.nativeElement);
207+
return thumb === Thumb.END ? thumbs[thumbs.length - 1] : thumbs[0];
208+
}
209+
210+
/** Gets the slider knob HTML element of the given thumb. */
211+
_getKnobElement(thumb: Thumb): HTMLElement {
212+
const knobs = this._knobs.toArray().map(e => e.nativeElement);
213+
return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0];
214+
}
215+
216+
/**
217+
* Gets the text representation of the given value.
218+
*
219+
* Uses the `displayWith` function if one has been provided. Otherwise, it just returns the
220+
* current numeric value as a string.
221+
*/
222+
_getValueIndicatorText(value: number): string {
223+
return this.displayWith ? this.displayWith(value) : value.toString();
224+
}
225+
226+
/** Gets the text representation of the current value of the given thumb. */
227+
_getValueIndicatorTextByThumb(thumb: Thumb): string {
228+
return this._getValueIndicatorText(this._getValue(thumb));
229+
}
230+
231+
/** Determines the class name for a HTML element. */
232+
_getTickMarkClass(tickMark: TickMark): string {
233+
return tickMark === TickMark.ACTIVE
234+
? 'mdc-slider__tick-mark--active'
235+
: 'mdc-slider__tick-mark--inactive';
236+
}
237+
238+
/** Returns an array of the thumb types that exist on the current slider instance. */
239+
_getThumbTypes(): Thumb[] {
240+
return this._isRange() ? [Thumb.START, Thumb.END] : [Thumb.END];
241+
}
242+
243+
static ngAcceptInputType_disabled: BooleanInput;
244+
static ngAcceptInputType_discrete: BooleanInput;
245+
static ngAcceptInputType_showTickMarks: BooleanInput;
246+
static ngAcceptInputType_min: NumberInput;
247+
static ngAcceptInputType_max: NumberInput;
248+
static ngAcceptInputType_step: NumberInput;
34249
}

src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@ <h2>MDC slide-toggle</h2>
104104
<mat-slide-toggle>with a label</mat-slide-toggle>
105105

106106
<h2>MDC Slider</h2>
107-
<mat-slider></mat-slider>
108-
<mat-slider value="50"></mat-slider>
109-
<mat-slider tickInterval="1" min="1" max="10" value="5" thumbLabel></mat-slider>
110-
<mat-slider disabled></mat-slider>
111107

112108
<h2>MDC Tabs</h2>
113109

0 commit comments

Comments
 (0)