Skip to content

Commit 82fb30f

Browse files
committed
fix(material-experimental/mdc-slider): code review changes
* remove circular dependency between MatSlider and MatSliderThumb * change the selectors for MatSliderThumb * add a comment describing why and how the value property on the MatSliderThumb works * delete min, max, and step @inputs from MatSliderThumb * remove _initialize property from MatSliderThumb * move all of the MatSliderThumb initialization logic into ngAfterViewInit * remove input initialization logic from MatSlider ngAfterViewInit * add input validation logic to MatSlider ngAfterViewInit * fix value indicator text bug where the DOM was not getting updated
1 parent 29f119e commit 82fb30f

File tree

4 files changed

+165
-125
lines changed

4 files changed

+165
-125
lines changed

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

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,51 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ChangeDetectorRef, ElementRef, Inject} from '@angular/core';
910
import {SpecificEventListener, EventType} from '@material/base';
1011
import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider';
11-
import {MatSlider} from './slider';
12+
import {MatSliderThumb, MAT_SLIDER} from './slider-thumb';
1213

14+
/**
15+
* This is a dummy interface that just contains the properties and methods of MatSlider that are
16+
* used by SliderAdapter. Rather than directly referencing MatSlider, we use this interface when
17+
* to avoid a circular dependency between MatSlider and SliderAdapter.
18+
*/
19+
interface MatSlider {
20+
_cdr: ChangeDetectorRef;
21+
min: number;
22+
max: number;
23+
disabled: boolean;
24+
_elementRef: ElementRef<HTMLElement>;
25+
_trackActive: ElementRef<HTMLElement>;
26+
_initialized: boolean;
27+
_tickMarks: TickMark[];
28+
_document: Document;
29+
_window: Window;
30+
displayWith: ((value: number) => string) | null;
31+
_getInput: (thumb: Thumb) => MatSliderThumb;
32+
_getKnobElement: (thumb: Thumb) => HTMLElement;
33+
_getThumbElement: (thumb: Thumb) => HTMLElement;
34+
_getInputElement: (thumb: Thumb) => HTMLInputElement;
35+
_setValue: (value: number, thumb: Thumb) => void;
36+
_setValueIndicatorText: (value: number, thumb: Thumb) => void;
37+
}
38+
39+
// TODO(wagnermaciel): Change to prototype methods once this PR is submitted.
40+
// https://github.com/material-components/material-components-web/pull/6256
1341
export class SliderAdapter implements MDCSliderAdapter {
14-
constructor(private readonly _delegate: MatSlider) {}
42+
constructor(@Inject(MAT_SLIDER) private readonly _delegate: MatSlider) {}
1543
hasClass = (className: string): boolean => {
16-
return this._delegate._hostElement.classList.contains(className);
44+
return this._delegate._elementRef.nativeElement.classList.contains(className);
1745
}
1846
addClass = (className: string): void => {
19-
this._delegate._hostElement.classList.add(className);
47+
this._delegate._elementRef.nativeElement.classList.add(className);
2048
}
2149
removeClass = (className: string): void => {
22-
this._delegate._hostElement.classList.remove(className);
50+
this._delegate._elementRef.nativeElement.classList.remove(className);
2351
}
2452
getAttribute = (attribute: string): string | null => {
25-
return this._delegate._hostElement.getAttribute(attribute);
53+
return this._delegate._elementRef.nativeElement.getAttribute(attribute);
2654
}
2755
addThumbClass = (className: string, thumb: Thumb): void => {
2856
this._delegate._getThumbElement(thumb).classList.add(className);
@@ -58,7 +86,7 @@ export class SliderAdapter implements MDCSliderAdapter {
5886
return this._delegate._getThumbElement(thumb).getBoundingClientRect();
5987
}
6088
getBoundingClientRect = (): ClientRect => {
61-
return this._delegate._hostElement.getBoundingClientRect();
89+
return this._delegate._elementRef.nativeElement.getBoundingClientRect();
6290
}
6391
isRTL = (): boolean => {
6492
// TODO(wagnermaciel): Actually implementing this.
@@ -84,10 +112,10 @@ export class SliderAdapter implements MDCSliderAdapter {
84112
}
85113
updateTickMarks = (tickMarks: TickMark[]): void => {
86114
this._delegate._tickMarks = tickMarks;
87-
this._delegate._cdr.markForCheck();
115+
this._delegate._cdr.detectChanges();
88116
}
89117
setPointerCapture = (pointerId: number): void => {
90-
this._delegate._hostElement.setPointerCapture(pointerId);
118+
this._delegate._elementRef.nativeElement.setPointerCapture(pointerId);
91119
}
92120
// We ignore emitChangeEvent and emitInputEvent because the slider inputs
93121
// are already exposed so users can just listen for those events directly themselves.
@@ -113,11 +141,11 @@ export class SliderAdapter implements MDCSliderAdapter {
113141
}
114142
registerEventHandler =
115143
<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void => {
116-
this._delegate._hostElement.addEventListener(evtType, handler);
144+
this._delegate._elementRef.nativeElement.addEventListener(evtType, handler);
117145
}
118146
deregisterEventHandler =
119147
<K extends EventType>(evtType: K, handler: SpecificEventListener<K>): void => {
120-
this._delegate._hostElement.removeEventListener(evtType, handler);
148+
this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler);
121149
}
122150
registerThumbEventHandler =
123151
<K extends EventType>(thumb: Thumb, evtType: K, handler: SpecificEventListener<K>): void => {

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

Lines changed: 70 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,38 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {NumberInput} from '@angular/cdk/coercion';
9+
import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
1010
import {DOCUMENT} from '@angular/common';
11-
import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core';
11+
import {
12+
AfterViewInit,
13+
Directive,
14+
ElementRef,
15+
EventEmitter,
16+
Inject,
17+
InjectionToken,
18+
Input,
19+
Output,
20+
} from '@angular/core';
1221
import {Thumb} from '@material/slider';
13-
import {MatSlider} from './slider';
22+
23+
/**
24+
* This is a dummy interface that just contains the properties and methods of MatSlider that are
25+
* used by MatSliderThumb. Rather than directly referencing MatSlider, we use this interface when
26+
* defining MAT_SLIDER to avoid a circular dependency between MatSlider and MatSliderThumb.
27+
*/
28+
interface MatSlider {
29+
min: number;
30+
max: number;
31+
disabled: boolean;
32+
_initialized: boolean;
33+
_getInput: (thumb: Thumb) => MatSliderThumb;
34+
_setValue: (value: number, thumb: Thumb) => void;
35+
}
36+
37+
/**
38+
* Injection token that can be used to inject instances of MatSlider.
39+
*/
40+
export const MAT_SLIDER = new InjectionToken<MatSlider>('MatSlider');
1441

1542
/**
1643
* Represents a drag event emitted by the MatSlider component.
@@ -33,61 +60,44 @@ export interface MatSliderDragEvent {
3360
* The native input used by the MatSlider.
3461
*/
3562
@Directive({
36-
selector: 'input[mat-slider-thumb]',
63+
selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]',
3764
host: {
3865
'class': 'mdc-slider__input',
3966
'type': 'range',
40-
'[min]': 'min',
41-
'[max]': 'max',
42-
'[step]': 'step',
43-
'[attr.value]': 'value',
4467
'(blur)': '_blur.emit()',
4568
'(focus)': '_focus.emit()',
46-
}
47-
}) export class MatSliderThumb {
69+
},
70+
}) export class MatSliderThumb implements AfterViewInit {
71+
72+
// ** IMPORTANT NOTE **
73+
//
74+
// The way `value` is implemented for MatSliderThumb goes against our standard practice. Normally
75+
// we would define a private variable `_value` as the source of truth for the value of the slider
76+
// thumb input. The source of truth for the value of the slider inputs has already been decided
77+
// for us by MDC to be the value attribute on the slider thumb inputs. This is because the MDC
78+
// foundation and adapter expect that the value attribute is the source of truth for the slider
79+
// inputs.
80+
//
81+
// Also, note that the value attribute is completely disconnected from the value property.
82+
4883
/** The current value of this slider input. */
4984
@Input()
50-
get value(): number { return Number(this._elementRef.nativeElement.getAttribute('value')); }
85+
get value(): number {
86+
return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value'));
87+
}
5188
set value(v: number) {
52-
this._initialized = true;
89+
const value = coerceNumberProperty(v);
5390

5491
// If the foundation has already been initialized, we need to
5592
// relay any value updates to it so that it can update the UI.
5693
if (this._slider._initialized) {
57-
this._slider._setValue(v, this.thumb);
94+
this._slider._setValue(value, this.thumb);
5895
} else {
5996
// Setup for the MDC foundation.
60-
this._elementRef.nativeElement.setAttribute('value', v.toString());
97+
this._elementRef.nativeElement.setAttribute('value', `${value}`);
6198
}
6299
}
63100

64-
/** The minimum value that this slider input can have. */
65-
@Input()
66-
get min(): number {
67-
return (this._slider._isRange() && this.thumb === Thumb.END)
68-
? this._slider._getValue(Thumb.START)
69-
: this._slider.min;
70-
}
71-
set min(v: number) { throw Error('Invalid attribute "min" on MatSliderThumb.'); }
72-
73-
/** The maximum value that this slider input can have. */
74-
@Input()
75-
get max(): number {
76-
return (this._slider._isRange() && this.thumb === Thumb.START)
77-
? this._slider._getValue(Thumb.END)
78-
: this._slider.max;
79-
}
80-
set max(v: number) { throw Error('Invalid attribute "max" on MatSliderThumb.'); }
81-
82-
/** The size of each increment between the values of the slider. */
83-
@Input()
84-
get step(): number { return 1; }
85-
set step(v: number) { throw Error('Invalid attribute "step" on MatSliderThumb.'); }
86-
87-
/** MDC Slider does not use the disabled attribute it's native inputs. */
88-
@Input()
89-
set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); }
90-
91101
/** Event emitted when the slider thumb starts being dragged. */
92102
@Output() readonly dragStart: EventEmitter<MatSliderDragEvent>
93103
= new EventEmitter<MatSliderDragEvent>();
@@ -105,35 +115,29 @@ export interface MatSliderDragEvent {
105115
/** Indicates which slider thumb this input corresponds to. */
106116
thumb: Thumb;
107117

108-
/** Whether the value of this slider thumb input has been set. */
109-
_initialized: boolean = false;
110-
111118
constructor(
112119
@Inject(DOCUMENT) private readonly _document: Document,
120+
@Inject(MAT_SLIDER) private readonly _slider: MatSlider,
113121
readonly _elementRef: ElementRef<HTMLInputElement>,
114-
private readonly _slider: MatSlider,
115-
) {
116-
// Initializing the min and max in the constructor guarantees that they will be
117-
// defined by the time the value gets set. If the range is not defined before we
118-
// try to set the value, we can run into the issue where the value is outside of
119-
// the default range and get capped to the default min or max.
120-
this._elementRef.nativeElement.min = this._slider.min.toString();
121-
this._elementRef.nativeElement.max = this._slider.max.toString();
122-
}
122+
) {}
123+
124+
ngAfterViewInit() {
125+
this.thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
126+
? Thumb.START
127+
: Thumb.END;
128+
129+
const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb')
130+
? this._slider._getInput(Thumb.START).value
131+
: this._slider.min;
132+
const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb')
133+
? this._slider._getInput(Thumb.END).value
134+
: this._slider.max;
135+
this._elementRef.nativeElement.min = `${min}`;
136+
this._elementRef.nativeElement.max = `${max}`;
123137

124-
/**
125-
* Sets up the initial state of the slider thumb input.
126-
*
127-
* This is needed because the slider thumb input is passed in via `ng-content`,
128-
* and therefore has no way of knowing which slider thumb it correspond to.
129-
*/
130-
_init(thumb: Thumb): void {
131-
this.thumb = thumb;
132-
133-
// If the value has not been initialized (i.e. no value was provided from
134-
// the user), determine the default value for the slider based on the given thumb.
135-
if (!this._initialized) {
136-
this.value = (this._slider._isRange() && thumb === Thumb.END)
138+
// Only set the default value if an initial value has not already been provided.
139+
if (!this._elementRef.nativeElement.hasAttribute('value')) {
140+
this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb')
137141
? this._slider.max
138142
: this._slider.min;
139143
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
<div class="mdc-slider__thumb" *ngFor="let thumb of _getThumbTypes()" #thumb>
1717
<div class="mdc-slider__value-indicator-container" *ngIf="discrete">
1818
<div class="mdc-slider__value-indicator">
19-
<span class="mdc-slider__value-indicator-text">
20-
{{_getValueIndicatorTextByThumb(thumb)}}
21-
</span>
19+
<span class="mdc-slider__value-indicator-text" #valueIndicatorTextElement></span>
2220
</div>
2321
</div>
2422
<div class="mdc-slider__thumb-knob" #knob></div>

0 commit comments

Comments
 (0)