diff --git a/src/material/slider/slider-input.ts b/src/material/slider/slider-input.ts index 6b8171f8cc0e..f357d9dbabc8 100644 --- a/src/material/slider/slider-input.ts +++ b/src/material/slider/slider-input.ts @@ -243,11 +243,21 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA _skipUIUpdate: boolean = false; /** Callback called when the slider input value changes. */ - private _onChangeFn: (value: any) => void = () => {}; + protected _onChangeFn: ((value: any) => void) | undefined; /** Callback called when the slider input has been touched. */ private _onTouchedFn: () => void = () => {}; + /** + * Whether the NgModel has been initialized. + * + * This flag is used to ignore ghost null calls to + * writeValue which can break slider initialization. + * + * See https://github.com/angular/angular/issues/14988. + */ + protected _isControlInitialized = false; + constructor( readonly _ngZone: NgZone, readonly _elementRef: ElementRef, @@ -328,7 +338,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA } _onInput(): void { - this._onChangeFn(this.value); + this._onChangeFn?.(this.value); // handles arrowing and updating the value when // a step is defined. if (this._slider.step || !this._isActive) { @@ -422,7 +432,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA this.value = value; this.valueChange.emit(this.value); - this._onChangeFn(this.value); + this._onChangeFn?.(this.value); this._slider._onValueChange(this); this._slider.step > 0 ? this._updateThumbUIByValue() @@ -500,7 +510,9 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA * @docs-private */ writeValue(value: any): void { - this.value = value; + if (this._isControlInitialized || value !== null) { + this.value = value; + } } /** @@ -510,6 +522,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA */ registerOnChange(fn: any): void { this._onChangeFn = fn; + this._isControlInitialized = true; } /** @@ -738,8 +751,10 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan * @docs-private */ override writeValue(value: any): void { - this.value = value; - this._updateWidthInactive(); - this._updateSibling(); + if (this._isControlInitialized || value !== null) { + this.value = value; + this._updateWidthInactive(); + this._updateSibling(); + } } } diff --git a/src/material/slider/slider.spec.ts b/src/material/slider/slider.spec.ts index a0e4b4389cc6..00fa6f063a67 100644 --- a/src/material/slider/slider.spec.ts +++ b/src/material/slider/slider.spec.ts @@ -1168,6 +1168,21 @@ describe('MDC-based MatSlider', () => { })); }); + describe('range slider w/ NgModel edge case', () => { + it('should initialize correctly despite NgModel `null` bug', fakeAsync(() => { + const fixture = createComponent(RangeSliderWithNgModelEdgeCase); + fixture.detectChanges(); + const sliderDebugElement = fixture.debugElement.query(By.directive(MatSlider)); + const slider = sliderDebugElement.componentInstance; + const startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb; + const endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb; + flush(); + console.log('result: ', startInput.value); + checkInput(startInput, {min: -1, max: -0.3, value: -0.7, translateX: 90}); + checkInput(endInput, {min: -0.7, max: 0, value: -0.3, translateX: 210}); + })); + }); + describe('slider as a custom form control', () => { let fixture: ComponentFixture; let slider: MatSlider; @@ -1617,6 +1632,22 @@ class RangeSliderWithNgModel { endVal: number | undefined = 100; } +@Component({ + template: ` + + + + + +`, + styles: SLIDER_STYLES, +}) +class RangeSliderWithNgModelEdgeCase { + @ViewChild(MatSlider) slider: MatSlider; + startValue: number = -0.7; + endValue: number = -0.3; +} + @Component({ template: ` diff --git a/tools/public_api_guard/material/slider.md b/tools/public_api_guard/material/slider.md index 8037af2b8a8e..a123d1bad943 100644 --- a/tools/public_api_guard/material/slider.md +++ b/tools/public_api_guard/material/slider.md @@ -218,6 +218,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA // (undocumented) _initValue(): void; _isActive: boolean; + protected _isControlInitialized: boolean; _isFocused: boolean; _knobRadius: number; get max(): number; @@ -232,6 +233,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA _onBlur(): void; // (undocumented) _onChange(): void; + protected _onChangeFn: ((value: any) => void) | undefined; // (undocumented) _onFocus(): void; // (undocumented)