From 390b5f01bd47889b39a839f4f03f89cc8c281df0 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Mon, 8 Feb 2021 14:59:08 -0800 Subject: [PATCH 01/12] feat(material-experimental/mdc-slider): implement the SliderAdapter * complete the core logic for MatSliderThumb and MatSlider --- .../mdc-slider/slider-adapter.ts | 193 ++++++++++-------- .../mdc-slider/slider-thumb.ts | 64 +++++- .../mdc-slider/slider.ts | 38 +++- 3 files changed, 194 insertions(+), 101 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index a3144d73f0d6..d0619595eb65 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -8,131 +8,146 @@ import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; +import {MatSlider} from './slider'; export class SliderAdapter implements MDCSliderAdapter { - hasClass(className: string): boolean { - throw Error('Method not implemented.'); + constructor(private readonly _delegate: MatSlider) {} + hasClass = (className: string): boolean => { + return this._delegate._hostElement.classList.contains(className); } - addClass(className: string): void { - throw Error('Method not implemented.'); + addClass = (className: string): void => { + this._delegate._hostElement.classList.add(className); } - removeClass(className: string): void { - throw Error('Method not implemented.'); + removeClass = (className: string): void => { + this._delegate._hostElement.classList.remove(className); } - getAttribute(attribute: string): string | null { - throw Error('Method not implemented.'); + getAttribute = (attribute: string): string | null => { + return this._delegate._hostElement.getAttribute(attribute); } - addThumbClass(className: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + addThumbClass = (className: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).classList.add(className); } - removeThumbClass(className: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + removeThumbClass = (className: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).classList.remove(className); } - getInputValue(thumb: Thumb): string { - throw Error('Method not implemented.'); + getInputValue = (thumb: Thumb): string => { + return this._delegate._getInputElement(thumb).value; } - setInputValue(value: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + setInputValue = (value: string, thumb: Thumb): void => { + this._delegate._getInputElement(thumb).value = value; } - getInputAttribute(attribute: string, thumb: Thumb): string | null { - throw Error('Method not implemented.'); + getInputAttribute = (attribute: string, thumb: Thumb): string | null => { + return this._delegate._getInputElement(thumb).getAttribute(attribute); } - setInputAttribute(attribute: string, value: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + setInputAttribute = (attribute: string, value: string, thumb: Thumb): void => { + this._delegate._getInputElement(thumb).setAttribute(attribute, value); } - removeInputAttribute(attribute: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + removeInputAttribute = (attribute: string, thumb: Thumb): void => { + this._delegate._getInputElement(thumb).removeAttribute(attribute); } - focusInput(thumb: Thumb): void { - throw Error('Method not implemented.'); + focusInput = (thumb: Thumb): void => { + this._delegate._getInputElement(thumb).focus(); } - isInputFocused(thumb: Thumb): boolean { - throw Error('Method not implemented.'); + isInputFocused = (thumb: Thumb): boolean => { + return this._delegate._getInput(thumb)._isFocused(); } - getThumbKnobWidth(thumb: Thumb): number { - throw Error('Method not implemented.'); + getThumbKnobWidth = (thumb: Thumb): number => { + return this._delegate._getKnobElement(thumb).getBoundingClientRect().width; } - getThumbBoundingClientRect(thumb: Thumb): ClientRect { - throw Error('Method not implemented.'); + getThumbBoundingClientRect = (thumb: Thumb): ClientRect => { + return this._delegate._getThumbElement(thumb).getBoundingClientRect(); } - getBoundingClientRect(): ClientRect { - throw Error('Method not implemented.'); + getBoundingClientRect = (): ClientRect => { + return this._delegate._hostElement.getBoundingClientRect(); } - isRTL(): boolean { - throw Error('Method not implemented.'); + isRTL = (): boolean => { + // TODO(wagnermaciel): Actually implementing this. + // throw Error('Method not implemented.'); + return false; } - setThumbStyleProperty(propertyName: string, value: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + setThumbStyleProperty = (propertyName: string, value: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).style.setProperty(propertyName, value); } - removeThumbStyleProperty(propertyName: string, thumb: Thumb): void { - throw Error('Method not implemented.'); + removeThumbStyleProperty = (propertyName: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).style.removeProperty(propertyName); } - setTrackActiveStyleProperty(propertyName: string, value: string): void { - throw Error('Method not implemented.'); + setTrackActiveStyleProperty = (propertyName: string, value: string): void => { + this._delegate._trackActive.nativeElement.style.setProperty(propertyName, value); } - removeTrackActiveStyleProperty(propertyName: string): void { - throw Error('Method not implemented.'); + removeTrackActiveStyleProperty = (propertyName: string): void => { + this._delegate._trackActive.nativeElement.style.removeProperty(propertyName); } - setValueIndicatorText(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); + setValueIndicatorText = (value: number, thumb: Thumb): void => { + this._delegate._setValueIndicatorText(value, thumb); } - getValueToAriaValueTextFn(): ((value: number) => string) | null { - throw Error('Method not implemented.'); + getValueToAriaValueTextFn = (): ((value: number) => string) | null => { + return this._delegate.displayWith; } - updateTickMarks(tickMarks: TickMark[]): void { - throw Error('Method not implemented.'); + updateTickMarks = (tickMarks: TickMark[]): void => { + this._delegate._tickMarks = tickMarks; + this._delegate._cdr.markForCheck(); } - setPointerCapture(pointerId: number): void { - throw Error('Method not implemented.'); + setPointerCapture = (pointerId: number): void => { + this._delegate._hostElement.setPointerCapture(pointerId); } - emitChangeEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); + emitChangeEvent = (value: number, thumb: Thumb): void => {}; + emitInputEvent = (value: number, thumb: Thumb): void => {}; + emitDragStartEvent = (value: number, thumb: Thumb): void => { + const input = this._delegate._getInput(thumb); + input.dragStart.emit({ + source: input, + parent: this._delegate, + value, + thumb, + }); } - emitInputEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); + emitDragEndEvent = (value: number, thumb: Thumb): void => { + const input = this._delegate._getInput(thumb); + input.dragEnd.emit({ + source: input, + parent: this._delegate, + value, + thumb, + }); } - emitDragStartEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); + registerEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._hostElement.addEventListener(evtType, handler); } - emitDragEndEvent(value: number, thumb: Thumb): void { - throw Error('Method not implemented.'); + deregisterEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._hostElement.removeEventListener(evtType, handler); } - registerEventHandler(evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + registerThumbEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumb).addEventListener(evtType, handler); } - deregisterEventHandler(evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + deregisterThumbEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumb).removeEventListener(evtType, handler); } - registerThumbEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + registerInputEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumb).addEventListener(evtType, handler); } - deregisterThumbEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + deregisterInputEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumb).removeEventListener(evtType, handler); } - registerInputEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + registerBodyEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._document.body.addEventListener(evtType, handler); } - deregisterInputEventHandler - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + deregisterBodyEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._document.body.removeEventListener(evtType, handler); } - registerBodyEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterBodyEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - registerWindowEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); - } - deregisterWindowEventHandler - (evtType: K, handler: SpecificEventListener): void { - throw Error('Method not implemented.'); + registerWindowEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._window.addEventListener(evtType, handler); + } + deregisterWindowEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._window.removeEventListener(evtType, handler); } } diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts index e9780a90ff2f..82f1d863f0a6 100644 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ b/src/material-experimental/mdc-slider/slider-thumb.ts @@ -47,18 +47,36 @@ export interface MatSliderDragEvent { }) export class MatSliderThumb { /** The current value of this slider input. */ @Input() - get value(): number { return this._value; } - set value(v: number) { this._value = v; } - private _value: number; + get value(): number { return Number(this._elementRef.nativeElement.getAttribute('value')); } + set value(v: number) { + this._initialized = true; + + // If the foundation has already been initialized, we need to + // relay any value updates to it so that it can update the UI. + if (this._slider._initialized) { + this._slider._setValue(v, this.thumb); + } else { + // Setup for the MDC foundation. + this._elementRef.nativeElement.setAttribute('value', v.toString()); + } + } /** The minimum value that this slider input can have. */ @Input() - get min(): number { return 0; } + get min(): number { + return (this._slider._isRange() && this.thumb === Thumb.END) + ? this._slider._getValue(Thumb.START) + : this._slider.min; + } set min(v: number) { throw Error('Invalid attribute "min" on MatSliderThumb.'); } /** The maximum value that this slider input can have. */ @Input() - get max(): number { return 100; } + get max(): number { + return (this._slider._isRange() && this.thumb === Thumb.START) + ? this._slider._getValue(Thumb.END) + : this._slider.max; + } set max(v: number) { throw Error('Invalid attribute "max" on MatSliderThumb.'); } /** The size of each increment between the values of the slider. */ @@ -87,10 +105,44 @@ export interface MatSliderDragEvent { /** Indicates which slider thumb this input corresponds to. */ thumb: Thumb; + /** Whether the value of this slider thumb input has been set. */ + _initialized: boolean = false; + constructor( @Inject(DOCUMENT) private readonly _document: Document, readonly _elementRef: ElementRef, - ) {} + private readonly _slider: MatSlider, + ) { + // Initializing the min and max in the constructor guarantees that they will be + // defined by the time the value gets set. If the range is not defined before we + // try to set the value, we can run into the issue where the value is outside of + // the default range and get capped to the default min or max. + this._elementRef.nativeElement.min = this._slider.min.toString(); + this._elementRef.nativeElement.max = this._slider.max.toString(); + } + + /** + * Sets up the initial state of the slider thumb input. + * + * This is needed because the slider thumb input is passed in via `ng-content`, + * and therefore has no way of knowing which slider thumb it correspond to. + */ + _init(thumb: Thumb): void { + this.thumb = thumb; + + // If the value has not been initialized (i.e. no value was provided from + // the user), determine the default value for the slider based on the given thumb. + if (!this._initialized) { + this.value = (this._slider._isRange() && thumb === Thumb.END) + ? this._slider.max + : this._slider.min; + } + + // Setup for the MDC foundation. + if (this._slider.disabled) { + this._elementRef.nativeElement.disabled = true; + } + } /** Returns true if this slider input currently has focus. */ _isFocused(): boolean { diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 2c06e7b736a8..a512212ca11d 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -120,7 +120,7 @@ export class MatSlider implements AfterViewInit, OnDestroy { @Input() displayWith: ((value: number) => string) | null; /** Instance of the MDC slider foundation for this slider. */ - private _foundation = new MDCSliderFoundation(new SliderAdapter()); + private _foundation = new MDCSliderFoundation(new SliderAdapter(this)); /** Whether the foundation has been initialized. */ _initialized: boolean = false; @@ -157,11 +157,13 @@ export class MatSlider implements AfterViewInit, OnDestroy { } ngAfterViewInit() { - this._foundation.init(); - if (this._platform.isBrowser) { - this._foundation.layout(); - } - this._initialized = true; + this._initInputs().then(() => { + this._foundation.init(); + if (this._platform.isBrowser) { + this._foundation.layout(); + } + this._initialized = true; + }); } ngOnDestroy() { @@ -170,6 +172,22 @@ export class MatSlider implements AfterViewInit, OnDestroy { } } + /** + * Sets up the initial state of the slider thumb inputs. + * + * The slider thumbs need this extra step because are passed in via `ng-content`, + * and therefore have no way of knowing which slider thumb they correspond to. + * + * This method needs to return a promise in order to avoid throwing an + * ExpressionChangedAfterItHasBeenCheckedError error. + */ + _initInputs(): Promise { + return Promise.resolve().then(() => { + this._inputs.get(0)?._init(this._isRange() ? Thumb.START : Thumb.END); + this._inputs.get(1)?._init(Thumb.END); + }); + } + /** Gets the current value of given slider thumb. */ _getValue(thumb: Thumb): number { return thumb === Thumb.START @@ -226,6 +244,14 @@ export class MatSlider implements AfterViewInit, OnDestroy { return this._getValueIndicatorText(this._getValue(thumb)); } + /** Sets the value indicator text of the given thumb with the given value. */ + _setValueIndicatorText(value: number, thumb: Thumb) { + const valueIndicatorText = this._getValueIndicatorText(value); + thumb === Thumb.END + ? this._endValueIndicatorText = valueIndicatorText + : this._startValueIndicatorText = valueIndicatorText; + } + /** Determines the class name for a HTML element. */ _getTickMarkClass(tickMark: TickMark): string { return tickMark === TickMark.ACTIVE From 29f119ee4e49d29720578cf9981b8531bf86dbf8 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Mon, 8 Feb 2021 17:46:16 -0800 Subject: [PATCH 02/12] fix(material-experimental/mdc-slider): code review changes * delete commented out code * add comment explaining why emitChangeEvent and emitInputEvent are ignored --- src/material-experimental/mdc-slider/slider-adapter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index d0619595eb65..7099d4fd02d0 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -62,7 +62,6 @@ export class SliderAdapter implements MDCSliderAdapter { } isRTL = (): boolean => { // TODO(wagnermaciel): Actually implementing this. - // throw Error('Method not implemented.'); return false; } setThumbStyleProperty = (propertyName: string, value: string, thumb: Thumb): void => { @@ -90,6 +89,8 @@ export class SliderAdapter implements MDCSliderAdapter { setPointerCapture = (pointerId: number): void => { this._delegate._hostElement.setPointerCapture(pointerId); } + // We ignore emitChangeEvent and emitInputEvent because the slider inputs + // are already exposed so users can just listen for those events directly themselves. emitChangeEvent = (value: number, thumb: Thumb): void => {}; emitInputEvent = (value: number, thumb: Thumb): void => {}; emitDragStartEvent = (value: number, thumb: Thumb): void => { From 82fb30f69c7b852312d34ca61d9f0c00ff4cd2e7 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Thu, 11 Feb 2021 11:23:11 -0800 Subject: [PATCH 03/12] 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 --- .../mdc-slider/slider-adapter.ts | 50 +++++-- .../mdc-slider/slider-thumb.ts | 136 +++++++++--------- .../mdc-slider/slider.html | 4 +- .../mdc-slider/slider.ts | 100 +++++++------ 4 files changed, 165 insertions(+), 125 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index 7099d4fd02d0..e6b0748cb707 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -6,23 +6,51 @@ * found in the LICENSE file at https://angular.io/license */ +import {ChangeDetectorRef, ElementRef, Inject} from '@angular/core'; import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; -import {MatSlider} from './slider'; +import {MatSliderThumb, MAT_SLIDER} from './slider-thumb'; +/** + * This is a dummy interface that just contains the properties and methods of MatSlider that are + * used by SliderAdapter. Rather than directly referencing MatSlider, we use this interface when + * to avoid a circular dependency between MatSlider and SliderAdapter. + */ +interface MatSlider { + _cdr: ChangeDetectorRef; + min: number; + max: number; + disabled: boolean; + _elementRef: ElementRef; + _trackActive: ElementRef; + _initialized: boolean; + _tickMarks: TickMark[]; + _document: Document; + _window: Window; + displayWith: ((value: number) => string) | null; + _getInput: (thumb: Thumb) => MatSliderThumb; + _getKnobElement: (thumb: Thumb) => HTMLElement; + _getThumbElement: (thumb: Thumb) => HTMLElement; + _getInputElement: (thumb: Thumb) => HTMLInputElement; + _setValue: (value: number, thumb: Thumb) => void; + _setValueIndicatorText: (value: number, thumb: Thumb) => void; +} + +// TODO(wagnermaciel): Change to prototype methods once this PR is submitted. +// https://github.com/material-components/material-components-web/pull/6256 export class SliderAdapter implements MDCSliderAdapter { - constructor(private readonly _delegate: MatSlider) {} + constructor(@Inject(MAT_SLIDER) private readonly _delegate: MatSlider) {} hasClass = (className: string): boolean => { - return this._delegate._hostElement.classList.contains(className); + return this._delegate._elementRef.nativeElement.classList.contains(className); } addClass = (className: string): void => { - this._delegate._hostElement.classList.add(className); + this._delegate._elementRef.nativeElement.classList.add(className); } removeClass = (className: string): void => { - this._delegate._hostElement.classList.remove(className); + this._delegate._elementRef.nativeElement.classList.remove(className); } getAttribute = (attribute: string): string | null => { - return this._delegate._hostElement.getAttribute(attribute); + return this._delegate._elementRef.nativeElement.getAttribute(attribute); } addThumbClass = (className: string, thumb: Thumb): void => { this._delegate._getThumbElement(thumb).classList.add(className); @@ -58,7 +86,7 @@ export class SliderAdapter implements MDCSliderAdapter { return this._delegate._getThumbElement(thumb).getBoundingClientRect(); } getBoundingClientRect = (): ClientRect => { - return this._delegate._hostElement.getBoundingClientRect(); + return this._delegate._elementRef.nativeElement.getBoundingClientRect(); } isRTL = (): boolean => { // TODO(wagnermaciel): Actually implementing this. @@ -84,10 +112,10 @@ export class SliderAdapter implements MDCSliderAdapter { } updateTickMarks = (tickMarks: TickMark[]): void => { this._delegate._tickMarks = tickMarks; - this._delegate._cdr.markForCheck(); + this._delegate._cdr.detectChanges(); } setPointerCapture = (pointerId: number): void => { - this._delegate._hostElement.setPointerCapture(pointerId); + this._delegate._elementRef.nativeElement.setPointerCapture(pointerId); } // We ignore emitChangeEvent and emitInputEvent because the slider inputs // are already exposed so users can just listen for those events directly themselves. @@ -113,11 +141,11 @@ export class SliderAdapter implements MDCSliderAdapter { } registerEventHandler = (evtType: K, handler: SpecificEventListener): void => { - this._delegate._hostElement.addEventListener(evtType, handler); + this._delegate._elementRef.nativeElement.addEventListener(evtType, handler); } deregisterEventHandler = (evtType: K, handler: SpecificEventListener): void => { - this._delegate._hostElement.removeEventListener(evtType, handler); + this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler); } registerThumbEventHandler = (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts index 82f1d863f0a6..53e8c9240c95 100644 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ b/src/material-experimental/mdc-slider/slider-thumb.ts @@ -6,11 +6,38 @@ * found in the LICENSE file at https://angular.io/license */ -import {NumberInput} from '@angular/cdk/coercion'; +import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; import {DOCUMENT} from '@angular/common'; -import {Directive, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core'; +import { + AfterViewInit, + Directive, + ElementRef, + EventEmitter, + Inject, + InjectionToken, + Input, + Output, +} from '@angular/core'; import {Thumb} from '@material/slider'; -import {MatSlider} from './slider'; + +/** + * This is a dummy interface that just contains the properties and methods of MatSlider that are + * used by MatSliderThumb. Rather than directly referencing MatSlider, we use this interface when + * defining MAT_SLIDER to avoid a circular dependency between MatSlider and MatSliderThumb. + */ +interface MatSlider { + min: number; + max: number; + disabled: boolean; + _initialized: boolean; + _getInput: (thumb: Thumb) => MatSliderThumb; + _setValue: (value: number, thumb: Thumb) => void; +} + +/** + * Injection token that can be used to inject instances of MatSlider. + */ +export const MAT_SLIDER = new InjectionToken('MatSlider'); /** * Represents a drag event emitted by the MatSlider component. @@ -33,61 +60,44 @@ export interface MatSliderDragEvent { * The native input used by the MatSlider. */ @Directive({ - selector: 'input[mat-slider-thumb]', + selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]', host: { 'class': 'mdc-slider__input', 'type': 'range', - '[min]': 'min', - '[max]': 'max', - '[step]': 'step', - '[attr.value]': 'value', '(blur)': '_blur.emit()', '(focus)': '_focus.emit()', - } -}) export class MatSliderThumb { + }, +}) export class MatSliderThumb implements AfterViewInit { + + // ** IMPORTANT NOTE ** + // + // The way `value` is implemented for MatSliderThumb goes against our standard practice. Normally + // we would define a private variable `_value` as the source of truth for the value of the slider + // thumb input. The source of truth for the value of the slider inputs has already been decided + // for us by MDC to be the value attribute on the slider thumb inputs. This is because the MDC + // foundation and adapter expect that the value attribute is the source of truth for the slider + // inputs. + // + // Also, note that the value attribute is completely disconnected from the value property. + /** The current value of this slider input. */ @Input() - get value(): number { return Number(this._elementRef.nativeElement.getAttribute('value')); } + get value(): number { + return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value')); + } set value(v: number) { - this._initialized = true; + const value = coerceNumberProperty(v); // If the foundation has already been initialized, we need to // relay any value updates to it so that it can update the UI. if (this._slider._initialized) { - this._slider._setValue(v, this.thumb); + this._slider._setValue(value, this.thumb); } else { // Setup for the MDC foundation. - this._elementRef.nativeElement.setAttribute('value', v.toString()); + this._elementRef.nativeElement.setAttribute('value', `${value}`); } } - /** The minimum value that this slider input can have. */ - @Input() - get min(): number { - return (this._slider._isRange() && this.thumb === Thumb.END) - ? this._slider._getValue(Thumb.START) - : this._slider.min; - } - set min(v: number) { throw Error('Invalid attribute "min" on MatSliderThumb.'); } - - /** The maximum value that this slider input can have. */ - @Input() - get max(): number { - return (this._slider._isRange() && this.thumb === Thumb.START) - ? this._slider._getValue(Thumb.END) - : this._slider.max; - } - set max(v: number) { throw Error('Invalid attribute "max" on MatSliderThumb.'); } - - /** The size of each increment between the values of the slider. */ - @Input() - get step(): number { return 1; } - set step(v: number) { throw Error('Invalid attribute "step" on MatSliderThumb.'); } - - /** MDC Slider does not use the disabled attribute it's native inputs. */ - @Input() - set disabled(v: boolean) { throw Error('Invalid attribute "disabled" on MatSliderThumb.'); } - /** Event emitted when the slider thumb starts being dragged. */ @Output() readonly dragStart: EventEmitter = new EventEmitter(); @@ -105,35 +115,29 @@ export interface MatSliderDragEvent { /** Indicates which slider thumb this input corresponds to. */ thumb: Thumb; - /** Whether the value of this slider thumb input has been set. */ - _initialized: boolean = false; - constructor( @Inject(DOCUMENT) private readonly _document: Document, + @Inject(MAT_SLIDER) private readonly _slider: MatSlider, readonly _elementRef: ElementRef, - private readonly _slider: MatSlider, - ) { - // Initializing the min and max in the constructor guarantees that they will be - // defined by the time the value gets set. If the range is not defined before we - // try to set the value, we can run into the issue where the value is outside of - // the default range and get capped to the default min or max. - this._elementRef.nativeElement.min = this._slider.min.toString(); - this._elementRef.nativeElement.max = this._slider.max.toString(); - } + ) {} + + ngAfterViewInit() { + this.thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? Thumb.START + : Thumb.END; + + const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? this._slider._getInput(Thumb.START).value + : this._slider.min; + const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? this._slider._getInput(Thumb.END).value + : this._slider.max; + this._elementRef.nativeElement.min = `${min}`; + this._elementRef.nativeElement.max = `${max}`; - /** - * Sets up the initial state of the slider thumb input. - * - * This is needed because the slider thumb input is passed in via `ng-content`, - * and therefore has no way of knowing which slider thumb it correspond to. - */ - _init(thumb: Thumb): void { - this.thumb = thumb; - - // If the value has not been initialized (i.e. no value was provided from - // the user), determine the default value for the slider based on the given thumb. - if (!this._initialized) { - this.value = (this._slider._isRange() && thumb === Thumb.END) + // Only set the default value if an initial value has not already been provided. + if (!this._elementRef.nativeElement.hasAttribute('value')) { + this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') ? this._slider.max : this._slider.min; } diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index 991a22bc27c4..2028d4c4affe 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -16,9 +16,7 @@
- - {{_getValueIndicatorTextByThumb(thumb)}} - +
diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index a512212ca11d..300c2321705d 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -31,7 +31,7 @@ import { } from '@angular/core'; import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; import {SliderAdapter} from './slider-adapter'; -import {MatSliderThumb} from './slider-thumb'; +import {MatSliderThumb, MAT_SLIDER} from './slider-thumb'; /** * Allows users to select from a range of values by moving the slider thumb. It is similar in @@ -51,6 +51,7 @@ import {MatSliderThumb} from './slider-thumb'; exportAs: 'matSlider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, + providers: [{provide: MAT_SLIDER, useExisting: MatSlider}], }) export class MatSlider implements AfterViewInit, OnDestroy { /** The slider thumb(s). */ @@ -59,6 +60,10 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** The slider thumb knob(s) */ @ViewChildren('knob') _knobs: QueryList>; + /** The span containing the slider thumb value indicator text */ + @ViewChildren('valueIndicatorTextElement') + _valueIndicatorTextElements: QueryList>; + /** The active section of the slider track. */ @ViewChild('trackActive') _trackActive: ElementRef; @@ -89,9 +94,7 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Whether the slider displays tick marks along the slider track. */ @Input() get showTickMarks(): boolean { return this._showTickMarks; } - set showTickMarks(v: boolean) { - this._showTickMarks = coerceBooleanProperty(v); - } + set showTickMarks(v: boolean) { this._showTickMarks = coerceBooleanProperty(v); } private _showTickMarks: boolean = false; /** The minimum value that the slider can have. */ @@ -125,12 +128,6 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Whether the foundation has been initialized. */ _initialized: boolean = false; - /** The string representation of the start thumbs value. */ - _startValueIndicatorText: string; - - /** The string representation of the end thumbs value. */ - _endValueIndicatorText: string; - /** The injected document if available or fallback to the global document reference. */ _document: Document; @@ -140,30 +137,25 @@ export class MatSlider implements AfterViewInit, OnDestroy { */ _window: Window; - /** The hosts native HTML element. */ - _hostElement: HTMLElement; - /** Used to keep track of & render the active & inactive tick marks on the slider track. */ _tickMarks: TickMark[]; constructor( readonly _cdr: ChangeDetectorRef, - private readonly _elementRef: ElementRef, + readonly _elementRef: ElementRef, private readonly _platform: Platform, @Inject(DOCUMENT) document: any) { this._document = document; this._window = this._document.defaultView || window; - this._hostElement = this._elementRef.nativeElement; } ngAfterViewInit() { - this._initInputs().then(() => { - this._foundation.init(); - if (this._platform.isBrowser) { - this._foundation.layout(); - } - this._initialized = true; - }); + this._validateInputs(); + this._foundation.init(); + if (this._platform.isBrowser) { + this._foundation.layout(); + } + this._initialized = true; } ngOnDestroy() { @@ -173,19 +165,21 @@ export class MatSlider implements AfterViewInit, OnDestroy { } /** - * Sets up the initial state of the slider thumb inputs. - * - * The slider thumbs need this extra step because are passed in via `ng-content`, - * and therefore have no way of knowing which slider thumb they correspond to. - * - * This method needs to return a promise in order to avoid throwing an - * ExpressionChangedAfterItHasBeenCheckedError error. + * Ensures that there is not an invalid configuration for the slider thumb inputs. */ - _initInputs(): Promise { - return Promise.resolve().then(() => { - this._inputs.get(0)?._init(this._isRange() ? Thumb.START : Thumb.END); - this._inputs.get(1)?._init(Thumb.END); - }); + _validateInputs(): void { + if (this._isRange()) { + if (!this._getInputElement(Thumb.START).hasAttribute('matSliderStartThumb')) { + this._throwInvalidInputConfigurationError('matSliderStartThumb'); + } + if (!this._getInputElement(Thumb.END).hasAttribute('matSliderEndThumb')) { + this._throwInvalidInputConfigurationError('matSliderEndThumb'); + } + } else { + if (!this._getInputElement(Thumb.END).hasAttribute('matSliderThumb')) { + this._throwInvalidInputConfigurationError('matSliderThumb'); + } + } } /** Gets the current value of given slider thumb. */ @@ -229,6 +223,12 @@ export class MatSlider implements AfterViewInit, OnDestroy { return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0]; } + /** Gets the slider knob HTML element of the given thumb. */ + _getValueIndicatorTextElement(thumb: Thumb): HTMLElement { + const elements = this._valueIndicatorTextElements.toArray().map(e => e.nativeElement); + return thumb === Thumb.END ? elements[elements.length - 1] : elements[0]; + } + /** * Gets the text representation of the given value. * @@ -236,20 +236,12 @@ export class MatSlider implements AfterViewInit, OnDestroy { * current numeric value as a string. */ _getValueIndicatorText(value: number): string { - return this.displayWith ? this.displayWith(value) : value.toString(); - } - - /** Gets the text representation of the current value of the given thumb. */ - _getValueIndicatorTextByThumb(thumb: Thumb): string { - return this._getValueIndicatorText(this._getValue(thumb)); + return this.displayWith ? this.displayWith(value) : `${value}`; } /** Sets the value indicator text of the given thumb with the given value. */ - _setValueIndicatorText(value: number, thumb: Thumb) { - const valueIndicatorText = this._getValueIndicatorText(value); - thumb === Thumb.END - ? this._endValueIndicatorText = valueIndicatorText - : this._startValueIndicatorText = valueIndicatorText; + _setValueIndicatorText(value: number, thumb: Thumb): void { + this._getValueIndicatorTextElement(thumb).textContent = this._getValueIndicatorText(value); } /** Determines the class name for a HTML element. */ @@ -264,6 +256,24 @@ export class MatSlider implements AfterViewInit, OnDestroy { return this._isRange() ? [Thumb.START, Thumb.END] : [Thumb.END]; } + _throwInvalidInputConfigurationError(missingSelector: string): void { + throw Error(`Invalid slider thumb input configuration! Missing a ${missingSelector}. + + Valid configurations are as follows: + + + + + + or + + + + + + `); + } + static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_discrete: BooleanInput; static ngAcceptInputType_showTickMarks: BooleanInput; From 238ffeab9c9c2510ed6efd581a4da74a44fd0b3e Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Thu, 11 Feb 2021 11:49:53 -0800 Subject: [PATCH 04/12] fix(material-experimental/mdc-slider): code review changes * add todo to remember to add the mdc-slider back into the kitchen sink ssr app * move _foundation.init() into the _platform.isBrowser check to avoid ssr problems --- src/material-experimental/mdc-slider/slider-adapter.ts | 2 ++ src/material-experimental/mdc-slider/slider.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index e6b0748cb707..649a4985c1c2 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -80,6 +80,8 @@ export class SliderAdapter implements MDCSliderAdapter { return this._delegate._getInput(thumb)._isFocused(); } getThumbKnobWidth = (thumb: Thumb): number => { + // TODO(wagnermaciel): Check if this causes issues for SSR + // once the mdc-slider is added back to the kitchen sink SSR app. return this._delegate._getKnobElement(thumb).getBoundingClientRect().width; } getThumbBoundingClientRect = (thumb: Thumb): ClientRect => { diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 300c2321705d..fcb33932c89c 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -151,11 +151,11 @@ export class MatSlider implements AfterViewInit, OnDestroy { ngAfterViewInit() { this._validateInputs(); - this._foundation.init(); if (this._platform.isBrowser) { + this._foundation.init(); this._foundation.layout(); + this._initialized = true; } - this._initialized = true; } ngOnDestroy() { From 62497a0561155405a890b27276b07bbc4be16ef6 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Fri, 12 Feb 2021 11:57:34 -0800 Subject: [PATCH 05/12] fix(material-experimental/mdc-slider): code review changes * make input validation only happen in ngDevMode * move _validateInputs() and _throwInvalidInputConfigurationError() out of MatSlider so they can be tree-shaken away in production mode * delete _getValueIndicatorText() and _getValueIndicatorTextElement() and add their logic to _setValueIndicatorText() * made getters more efficient * deleted unused _getValue() method * move export class MatSliderThumb ... to its own separate line * moved MatSliderThumb value initialization to the constructor to avoid race conditions when setting the min and max * set the value property in ngAfterViewInit for MatSliderThumb --- .../mdc-slider/slider-thumb.ts | 30 +++-- .../mdc-slider/slider.ts | 123 +++++++++--------- 2 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts index 53e8c9240c95..9b6f324fcf6f 100644 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ b/src/material-experimental/mdc-slider/slider-thumb.ts @@ -67,7 +67,8 @@ export interface MatSliderDragEvent { '(blur)': '_blur.emit()', '(focus)': '_focus.emit()', }, -}) export class MatSliderThumb implements AfterViewInit { +}) +export class MatSliderThumb implements AfterViewInit { // ** IMPORTANT NOTE ** // @@ -119,13 +120,22 @@ export interface MatSliderDragEvent { @Inject(DOCUMENT) private readonly _document: Document, @Inject(MAT_SLIDER) private readonly _slider: MatSlider, readonly _elementRef: ElementRef, - ) {} + ) { + this.thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? Thumb.START + : Thumb.END; + + // Only set the default value if an initial value has not already been provided. + // Note that we are only setting the value attribute at this point. We cannot set the value + // property yet because the min and max have not been set. + if (!_elementRef.nativeElement.hasAttribute('value')) { + this.value = _elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? _slider.max + : _slider.min; + } + } ngAfterViewInit() { - this.thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') - ? Thumb.START - : Thumb.END; - const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') ? this._slider._getInput(Thumb.START).value : this._slider.min; @@ -135,12 +145,8 @@ export interface MatSliderDragEvent { this._elementRef.nativeElement.min = `${min}`; this._elementRef.nativeElement.max = `${max}`; - // Only set the default value if an initial value has not already been provided. - if (!this._elementRef.nativeElement.hasAttribute('value')) { - this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') - ? this._slider.max - : this._slider.min; - } + // We can now set the property value because the min and max have now been set. + this._elementRef.nativeElement.value = `${this.value}`; // Setup for the MDC foundation. if (this._slider.disabled) { diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index fcb33932c89c..a470f65e5248 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -150,7 +150,13 @@ export class MatSlider implements AfterViewInit, OnDestroy { } ngAfterViewInit() { - this._validateInputs(); + if (typeof ngDevMode === 'undefined' || ngDevMode) { + _validateInputs( + this._isRange(), + this._getInputElement(Thumb.START), + this._getInputElement(Thumb.END), + ); + } if (this._platform.isBrowser) { this._foundation.init(); this._foundation.layout(); @@ -164,31 +170,6 @@ export class MatSlider implements AfterViewInit, OnDestroy { } } - /** - * Ensures that there is not an invalid configuration for the slider thumb inputs. - */ - _validateInputs(): void { - if (this._isRange()) { - if (!this._getInputElement(Thumb.START).hasAttribute('matSliderStartThumb')) { - this._throwInvalidInputConfigurationError('matSliderStartThumb'); - } - if (!this._getInputElement(Thumb.END).hasAttribute('matSliderEndThumb')) { - this._throwInvalidInputConfigurationError('matSliderEndThumb'); - } - } else { - if (!this._getInputElement(Thumb.END).hasAttribute('matSliderThumb')) { - this._throwInvalidInputConfigurationError('matSliderThumb'); - } - } - } - - /** Gets the current value of given slider thumb. */ - _getValue(thumb: Thumb): number { - return thumb === Thumb.START - ? this._foundation.getValueStart() - : this._foundation.getValue(); - } - /** Sets the value of a slider thumb. */ _setValue(value: number, thumb: Thumb): void { thumb === Thumb.START @@ -203,7 +184,7 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Gets the slider thumb input of the given thumb. */ _getInput(thumb: Thumb): MatSliderThumb { - return thumb === Thumb.END ? this._inputs.get(this._inputs.length - 1)! : this._inputs.get(0)!; + return thumb === Thumb.END ? this._inputs.last! : this._inputs.first!; } /** Gets the slider thumb HTML input element of the given thumb. */ @@ -213,35 +194,28 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Gets the slider thumb HTML element of the given thumb. */ _getThumbElement(thumb: Thumb): HTMLElement { - const thumbs = this._thumbs.toArray().map(e => e.nativeElement); - return thumb === Thumb.END ? thumbs[thumbs.length - 1] : thumbs[0]; + const thumbElementRef = thumb === Thumb.END ? this._thumbs.last : this._thumbs.first; + return thumbElementRef.nativeElement; } /** Gets the slider knob HTML element of the given thumb. */ _getKnobElement(thumb: Thumb): HTMLElement { - const knobs = this._knobs.toArray().map(e => e.nativeElement); - return thumb === Thumb.END ? knobs[knobs.length - 1] : knobs[0]; + const knobElementRef = thumb === Thumb.END ? this._knobs.last : this._knobs.first; + return knobElementRef.nativeElement; } - /** Gets the slider knob HTML element of the given thumb. */ - _getValueIndicatorTextElement(thumb: Thumb): HTMLElement { - const elements = this._valueIndicatorTextElements.toArray().map(e => e.nativeElement); - return thumb === Thumb.END ? elements[elements.length - 1] : elements[0]; - } - /** - * Gets the text representation of the given value. + * Sets the value indicator text of the given thumb using the given value. * - * Uses the `displayWith` function if one has been provided. Otherwise, it just returns the - * current numeric value as a string. + * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the + * numeric value as a string. */ - _getValueIndicatorText(value: number): string { - return this.displayWith ? this.displayWith(value) : `${value}`; - } - - /** Sets the value indicator text of the given thumb with the given value. */ _setValueIndicatorText(value: number, thumb: Thumb): void { - this._getValueIndicatorTextElement(thumb).textContent = this._getValueIndicatorText(value); + const valueIndicatorTextElementRef = thumb === Thumb.END + ? this._valueIndicatorTextElements.last + : this._valueIndicatorTextElements.first; + const valueText = this.displayWith ? this.displayWith(value) : `${value}`; + valueIndicatorTextElementRef.nativeElement.textContent = valueText; } /** Determines the class name for a HTML element. */ @@ -256,24 +230,6 @@ export class MatSlider implements AfterViewInit, OnDestroy { return this._isRange() ? [Thumb.START, Thumb.END] : [Thumb.END]; } - _throwInvalidInputConfigurationError(missingSelector: string): void { - throw Error(`Invalid slider thumb input configuration! Missing a ${missingSelector}. - - Valid configurations are as follows: - - - - - - or - - - - - - `); - } - static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_discrete: BooleanInput; static ngAcceptInputType_showTickMarks: BooleanInput; @@ -281,3 +237,42 @@ export class MatSlider implements AfterViewInit, OnDestroy { static ngAcceptInputType_max: NumberInput; static ngAcceptInputType_step: NumberInput; } + +/** + * Ensures that there is not an invalid configuration for the slider thumb inputs. + */ +function _validateInputs( + isRange: boolean, + startInputElement: HTMLInputElement, + endInputElement: HTMLInputElement): void { + if (isRange) { + if (!startInputElement!.hasAttribute('matSliderStartThumb')) { + _throwInvalidInputConfigurationError(); + } + if (!endInputElement.hasAttribute('matSliderEndThumb')) { + _throwInvalidInputConfigurationError(); + } + } else { + if (!endInputElement.hasAttribute('matSliderThumb')) { + _throwInvalidInputConfigurationError(); + } + } +} + +function _throwInvalidInputConfigurationError(): void { + throw Error(`Invalid slider thumb input configuration! + + Valid configurations are as follows: + + + + + + or + + + + + + `); +} From 5c9b975e518dacd69c68bf6bc2e2dc8a2dd95901 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Tue, 16 Feb 2021 08:19:46 -0800 Subject: [PATCH 06/12] fix(material-experimental/mdc-slider): untangle circular deps * move dummy MatSlider interfaces from slider-adapter and slider-thumb to slider-interface * create MatSliderThumb and MatSliderDragEvent dummy interfaces to avoid a circular dep between slider-thumb and slider-interface when defining the dummy MatSliderThumb interface * make MatSlider implement the dummy _MatSliderInterface --- .../mdc-slider/slider-adapter.ts | 45 +---- .../mdc-slider/slider-interface.ts | 165 ++++++++++++++++++ .../mdc-slider/slider-thumb.ts | 31 +--- .../mdc-slider/slider.ts | 14 +- 4 files changed, 184 insertions(+), 71 deletions(-) create mode 100644 src/material-experimental/mdc-slider/slider-interface.ts diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index 649a4985c1c2..cd615193d88a 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -6,40 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, ElementRef, Inject} from '@angular/core'; +import {Inject} from '@angular/core'; import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; -import {MatSliderThumb, MAT_SLIDER} from './slider-thumb'; - -/** - * This is a dummy interface that just contains the properties and methods of MatSlider that are - * used by SliderAdapter. Rather than directly referencing MatSlider, we use this interface when - * to avoid a circular dependency between MatSlider and SliderAdapter. - */ -interface MatSlider { - _cdr: ChangeDetectorRef; - min: number; - max: number; - disabled: boolean; - _elementRef: ElementRef; - _trackActive: ElementRef; - _initialized: boolean; - _tickMarks: TickMark[]; - _document: Document; - _window: Window; - displayWith: ((value: number) => string) | null; - _getInput: (thumb: Thumb) => MatSliderThumb; - _getKnobElement: (thumb: Thumb) => HTMLElement; - _getThumbElement: (thumb: Thumb) => HTMLElement; - _getInputElement: (thumb: Thumb) => HTMLInputElement; - _setValue: (value: number, thumb: Thumb) => void; - _setValueIndicatorText: (value: number, thumb: Thumb) => void; -} +import {_MatSliderInterface, MAT_SLIDER} from './slider-interface'; // TODO(wagnermaciel): Change to prototype methods once this PR is submitted. // https://github.com/material-components/material-components-web/pull/6256 export class SliderAdapter implements MDCSliderAdapter { - constructor(@Inject(MAT_SLIDER) private readonly _delegate: MatSlider) {} + constructor(@Inject(MAT_SLIDER) private readonly _delegate: _MatSliderInterface) {} hasClass = (className: string): boolean => { return this._delegate._elementRef.nativeElement.classList.contains(className); } @@ -125,21 +100,11 @@ export class SliderAdapter implements MDCSliderAdapter { emitInputEvent = (value: number, thumb: Thumb): void => {}; emitDragStartEvent = (value: number, thumb: Thumb): void => { const input = this._delegate._getInput(thumb); - input.dragStart.emit({ - source: input, - parent: this._delegate, - value, - thumb, - }); + input.dragStart.emit({ source: input, value, thumb }); } emitDragEndEvent = (value: number, thumb: Thumb): void => { const input = this._delegate._getInput(thumb); - input.dragEnd.emit({ - source: input, - parent: this._delegate, - value, - thumb, - }); + input.dragEnd.emit({ source: input, value, thumb }); } registerEventHandler = (evtType: K, handler: SpecificEventListener): void => { diff --git a/src/material-experimental/mdc-slider/slider-interface.ts b/src/material-experimental/mdc-slider/slider-interface.ts new file mode 100644 index 000000000000..4ea1e6d0cf02 --- /dev/null +++ b/src/material-experimental/mdc-slider/slider-interface.ts @@ -0,0 +1,165 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + ChangeDetectorRef, + ElementRef, + EventEmitter, + InjectionToken, + QueryList, +} from '@angular/core'; +import {Thumb, TickMark} from '@material/slider'; + +/** + * Represents a drag event emitted by the MatSlider component. + */ +interface _MatSliderDragEventInterface { + /** The MatSliderThumb that was interacted with. */ + source: _MatSliderThumbInterface; + + /** The current value of the slider. */ + value: number; + + /** The thumb that was interacted with. */ + thumb: Thumb; +} + +export interface _MatSliderThumbInterface { + /** The current value of this slider input. */ + value: number; + + /** Event emitted when the slider thumb starts being dragged. */ + dragStart: EventEmitter<_MatSliderDragEventInterface>; + + /** Event emitted when the slider thumb stops being dragged. */ + dragEnd: EventEmitter<_MatSliderDragEventInterface>; + + /** Event emitted every time the MatSliderThumb is blurred. */ + _blur: EventEmitter; + + /** Event emitted every time the MatSliderThumb is focused. */ + _focus: EventEmitter; + + /** Indicates which slider thumb this input corresponds to. */ + thumb: Thumb; + + /** A reference to MatSliderThumbs root/host element. */ + _elementRef: ElementRef; + + /** The injected document if available or fallback to the global document reference. */ + _document: Document; + + /** Returns true if this slider input currently has focus. */ + _isFocused: () => boolean; +} + + +/** + * This is a dummy interface that just contains the properties and methods of MatSlider that are + * used by MatSliderThumb. Rather than directly referencing MatSlider, we use this interface when + * defining MAT_SLIDER to avoid a circular dependency between MatSlider and MatSliderThumb. + */ +export interface _MatSliderInterface { + /** The slider thumb(s). */ + _thumbs: QueryList>; + + /** The slider thumb knob(s) */ + _knobs: QueryList>; + + /** The span containing the slider thumb value indicator text */ + _valueIndicatorTextElements: QueryList>; + + /** The active section of the slider track. */ + _trackActive: ElementRef; + + /** The sliders hidden range input(s). */ + _inputs: QueryList<_MatSliderThumbInterface>; + + /** Whether the slider is disabled. */ + disabled: boolean; + + /** Whether the slider displays a numeric value label upon pressing the thumb. */ + discrete: boolean; + + /** Whether the slider displays tick marks along the slider track. */ + showTickMarks: boolean; + + /** The minimum value that the slider can have. */ + min: number; + + /** The maximum value that the slider can have. */ + max: number; + + /** The values at which the thumb will snap. */ + step: number; + + /** + * Function that will be used to format the value before it is displayed + * in the thumb label. Can be used to format very large number in order + * for them to fit into the slider thumb. + */ + displayWith: ((value: number) => string) | null; + + /** Whether the foundation has been initialized. */ + _initialized: boolean; + + /** The injected document if available or fallback to the global document reference. */ + _document: Document; + + /** + * The defaultView of the injected document if + * available or fallback to global window reference. + */ + _window: Window; + + /** Used to keep track of & render the active & inactive tick marks on the slider track. */ + _tickMarks: TickMark[]; + + /** The change detector ref. */ + _cdr: ChangeDetectorRef; + + /** A reference to MatSliders root/host element. */ + _elementRef: ElementRef; + + /** Sets the value of a slider thumb. */ + _setValue: (value: number, thumb: Thumb) => void; + + /** Whether this is a ranged slider. */ + _isRange: () => boolean; + + /** Gets the slider thumb input of the given thumb. */ + _getInput: (thumb: Thumb) => _MatSliderThumbInterface; + + /** Gets the slider thumb HTML input element of the given thumb. */ + _getInputElement: (thumb: Thumb) => HTMLInputElement; + + /** Gets the slider thumb HTML element of the given thumb. */ + _getThumbElement: (thumb: Thumb) => HTMLElement; + + /** Gets the slider knob HTML element of the given thumb. */ + _getKnobElement: (thumb: Thumb) => HTMLElement; + + /** + * Sets the value indicator text of the given thumb using the given value. + * + * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the + * numeric value as a string. + */ + _setValueIndicatorText: (value: number, thumb: Thumb) => void; + + /** Determines the class name for a HTML element. */ + _getTickMarkClass: (tickMark: TickMark) => string; + + /** Returns an array of the thumb types that exist on the current slider instance. */ + _getThumbTypes: () => Thumb[]; +} + +/** + * Injection token that can be used to inject instances of MatSlider. + */ +export const MAT_SLIDER = new InjectionToken<_MatSliderInterface>('MatSlider'); diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts index 9b6f324fcf6f..a43a5dc7cc01 100644 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ b/src/material-experimental/mdc-slider/slider-thumb.ts @@ -14,30 +14,11 @@ import { ElementRef, EventEmitter, Inject, - InjectionToken, Input, Output, } from '@angular/core'; import {Thumb} from '@material/slider'; - -/** - * This is a dummy interface that just contains the properties and methods of MatSlider that are - * used by MatSliderThumb. Rather than directly referencing MatSlider, we use this interface when - * defining MAT_SLIDER to avoid a circular dependency between MatSlider and MatSliderThumb. - */ -interface MatSlider { - min: number; - max: number; - disabled: boolean; - _initialized: boolean; - _getInput: (thumb: Thumb) => MatSliderThumb; - _setValue: (value: number, thumb: Thumb) => void; -} - -/** - * Injection token that can be used to inject instances of MatSlider. - */ -export const MAT_SLIDER = new InjectionToken('MatSlider'); +import {_MatSliderInterface, MAT_SLIDER} from './slider-interface'; /** * Represents a drag event emitted by the MatSlider component. @@ -46,9 +27,6 @@ export interface MatSliderDragEvent { /** The MatSliderThumb that was interacted with. */ source: MatSliderThumb; - /** The parent MatSlider that was interacted with. */ - parent: MatSlider; - /** The current value of the slider. */ value: number; @@ -116,11 +94,14 @@ export class MatSliderThumb implements AfterViewInit { /** Indicates which slider thumb this input corresponds to. */ thumb: Thumb; + private _document: Document; + constructor( - @Inject(DOCUMENT) private readonly _document: Document, - @Inject(MAT_SLIDER) private readonly _slider: MatSlider, + @Inject(DOCUMENT) document: any, + @Inject(MAT_SLIDER) private readonly _slider: _MatSliderInterface, readonly _elementRef: ElementRef, ) { + this._document = document; this.thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') ? Thumb.START : Thumb.END; diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index a470f65e5248..d71761747f0e 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -31,7 +31,8 @@ import { } from '@angular/core'; import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; import {SliderAdapter} from './slider-adapter'; -import {MatSliderThumb, MAT_SLIDER} from './slider-thumb'; +import {MatSliderThumb} from './slider-thumb'; +import {_MatSliderInterface, _MatSliderThumbInterface, MAT_SLIDER} from './slider-interface'; /** * Allows users to select from a range of values by moving the slider thumb. It is similar in @@ -53,7 +54,7 @@ import {MatSliderThumb, MAT_SLIDER} from './slider-thumb'; encapsulation: ViewEncapsulation.None, providers: [{provide: MAT_SLIDER, useExisting: MatSlider}], }) -export class MatSlider implements AfterViewInit, OnDestroy { +export class MatSlider implements _MatSliderInterface, AfterViewInit, OnDestroy { /** The slider thumb(s). */ @ViewChildren('thumb') _thumbs: QueryList>; @@ -68,7 +69,8 @@ export class MatSlider implements AfterViewInit, OnDestroy { @ViewChild('trackActive') _trackActive: ElementRef; /** The sliders hidden range input(s). */ - @ContentChildren(MatSliderThumb, {descendants: false}) _inputs: QueryList; + @ContentChildren(MatSliderThumb, {descendants: false}) + _inputs: QueryList<_MatSliderThumbInterface>; /** Whether the slider is disabled. */ @Input() @@ -101,13 +103,13 @@ export class MatSlider implements AfterViewInit, OnDestroy { @Input() get min(): number { return this._min; } set min(v: number) { this._min = coerceNumberProperty(v, this._min); } - private _min = 0; + private _min: number = 0; /** The maximum value that the slider can have. */ @Input() get max(): number { return this._max; } set max(v: number) { this._max = coerceNumberProperty(v, this._max); } - private _max = 100; + private _max: number = 100; /** The values at which the thumb will snap. */ @Input() @@ -183,7 +185,7 @@ export class MatSlider implements AfterViewInit, OnDestroy { } /** Gets the slider thumb input of the given thumb. */ - _getInput(thumb: Thumb): MatSliderThumb { + _getInput(thumb: Thumb): _MatSliderThumbInterface { return thumb === Thumb.END ? this._inputs.last! : this._inputs.first!; } From 2a98bf71447dad01d4aa98cd2538532a2fafe056 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Tue, 16 Feb 2021 08:23:57 -0800 Subject: [PATCH 07/12] fix(material-experimental/mdc-slider): fix comments in slider-adapter * add class description jsdoc * move comment explaining why we use arrow fns --- src/material-experimental/mdc-slider/slider-adapter.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index cd615193d88a..9bec91179337 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -11,10 +11,14 @@ import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; import {_MatSliderInterface, MAT_SLIDER} from './slider-interface'; -// TODO(wagnermaciel): Change to prototype methods once this PR is submitted. -// https://github.com/material-components/material-components-web/pull/6256 +/** The MDCSliderAdapter implementation. */ export class SliderAdapter implements MDCSliderAdapter { constructor(@Inject(MAT_SLIDER) private readonly _delegate: _MatSliderInterface) {} + + // We manually assign functions instead of using prototype methods because + // MDC clobbers the values otherwise. + // See https://github.com/material-components/material-components-web/pull/6256 + hasClass = (className: string): boolean => { return this._delegate._elementRef.nativeElement.classList.contains(className); } From 677d20e1461d122f69a36c5da4b9362a8f99f566 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Tue, 16 Feb 2021 14:07:20 -0800 Subject: [PATCH 08/12] fix(material-experimental/mdc-slider): implement a better solution for change detection problem * use markForCheck() instead of detectChanges() in updateTickMarks() * call detectChanges() once at the end of ngAfterViewInit() in MatSlider with a thorough comment explaining why it is needed --- .../mdc-slider/slider-adapter.ts | 2 +- src/material-experimental/mdc-slider/slider.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts index 9bec91179337..6c1cd7475fcd 100644 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ b/src/material-experimental/mdc-slider/slider-adapter.ts @@ -93,7 +93,7 @@ export class SliderAdapter implements MDCSliderAdapter { } updateTickMarks = (tickMarks: TickMark[]): void => { this._delegate._tickMarks = tickMarks; - this._delegate._cdr.detectChanges(); + this._delegate._cdr.markForCheck(); } setPointerCapture = (pointerId: number): void => { this._delegate._elementRef.nativeElement.setPointerCapture(pointerId); diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index d71761747f0e..a6489787472b 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -164,6 +164,17 @@ export class MatSlider implements _MatSliderInterface, AfterViewInit, OnDestroy this._foundation.layout(); this._initialized = true; } + // The MDC foundation requires access to the view and content children of the MatSlider. In + // order to access the view and content children of MatSlider we need to wait until change + // detection runs and materializes them. That is why we call init() and layout() in + // ngAfterViewInit(). + // + // The MDC foundation then uses the information it gathers from the DOM to compute an initial + // value for the tickMarks array. It then tries to update the component data, but because it is + // updating the component data AFTER change detection already ran, we will get a changed after + // checked error. Because of this, we need to force change detection to update the UI with the + // new state. + this._cdr.detectChanges(); } ngOnDestroy() { From d04141b5d72d6cd25d266866b11e774e36b4acae Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Tue, 16 Feb 2021 14:55:08 -0800 Subject: [PATCH 09/12] fix(material-experimental/mdc-slider): collapse everything into slider.ts * delete slider-adapter.ts * delete slider-interface.ts * delete slider-thumb.ts * move SliderAdapter implementation to slider.ts * move MatSliderThumb implementation to slider.ts * go back to using direct references instead of interfaces * fix imports in module.ts * fix imports in public-api.ts --- .../mdc-slider/module.ts | 3 +- .../mdc-slider/public-api.ts | 3 +- .../mdc-slider/slider-adapter.ts | 153 ---------- .../mdc-slider/slider-interface.ts | 165 ---------- .../mdc-slider/slider-thumb.ts | 144 --------- .../mdc-slider/slider.ts | 284 +++++++++++++++++- 6 files changed, 278 insertions(+), 474 deletions(-) delete mode 100644 src/material-experimental/mdc-slider/slider-adapter.ts delete mode 100644 src/material-experimental/mdc-slider/slider-interface.ts delete mode 100644 src/material-experimental/mdc-slider/slider-thumb.ts diff --git a/src/material-experimental/mdc-slider/module.ts b/src/material-experimental/mdc-slider/module.ts index 4beea1a3b390..ba3e3367ad34 100644 --- a/src/material-experimental/mdc-slider/module.ts +++ b/src/material-experimental/mdc-slider/module.ts @@ -9,8 +9,7 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {MatCommonModule} from '@angular/material-experimental/mdc-core'; -import {MatSlider} from './slider'; -import {MatSliderThumb} from './slider-thumb'; +import {MatSlider, MatSliderThumb} from './slider'; @NgModule({ imports: [MatCommonModule, CommonModule], diff --git a/src/material-experimental/mdc-slider/public-api.ts b/src/material-experimental/mdc-slider/public-api.ts index ee39eb6b8adf..294dbb8a1be7 100644 --- a/src/material-experimental/mdc-slider/public-api.ts +++ b/src/material-experimental/mdc-slider/public-api.ts @@ -6,6 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export {MatSlider} from './slider'; -export {MatSliderThumb} from './slider-thumb'; +export {MatSlider, MatSliderThumb, MatSliderDragEvent} from './slider'; export {MatSliderModule} from './module'; diff --git a/src/material-experimental/mdc-slider/slider-adapter.ts b/src/material-experimental/mdc-slider/slider-adapter.ts deleted file mode 100644 index 6c1cd7475fcd..000000000000 --- a/src/material-experimental/mdc-slider/slider-adapter.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Inject} from '@angular/core'; -import {SpecificEventListener, EventType} from '@material/base'; -import {MDCSliderAdapter, Thumb, TickMark} from '@material/slider'; -import {_MatSliderInterface, MAT_SLIDER} from './slider-interface'; - -/** The MDCSliderAdapter implementation. */ -export class SliderAdapter implements MDCSliderAdapter { - constructor(@Inject(MAT_SLIDER) private readonly _delegate: _MatSliderInterface) {} - - // We manually assign functions instead of using prototype methods because - // MDC clobbers the values otherwise. - // See https://github.com/material-components/material-components-web/pull/6256 - - hasClass = (className: string): boolean => { - return this._delegate._elementRef.nativeElement.classList.contains(className); - } - addClass = (className: string): void => { - this._delegate._elementRef.nativeElement.classList.add(className); - } - removeClass = (className: string): void => { - this._delegate._elementRef.nativeElement.classList.remove(className); - } - getAttribute = (attribute: string): string | null => { - return this._delegate._elementRef.nativeElement.getAttribute(attribute); - } - addThumbClass = (className: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).classList.add(className); - } - removeThumbClass = (className: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).classList.remove(className); - } - getInputValue = (thumb: Thumb): string => { - return this._delegate._getInputElement(thumb).value; - } - setInputValue = (value: string, thumb: Thumb): void => { - this._delegate._getInputElement(thumb).value = value; - } - getInputAttribute = (attribute: string, thumb: Thumb): string | null => { - return this._delegate._getInputElement(thumb).getAttribute(attribute); - } - setInputAttribute = (attribute: string, value: string, thumb: Thumb): void => { - this._delegate._getInputElement(thumb).setAttribute(attribute, value); - } - removeInputAttribute = (attribute: string, thumb: Thumb): void => { - this._delegate._getInputElement(thumb).removeAttribute(attribute); - } - focusInput = (thumb: Thumb): void => { - this._delegate._getInputElement(thumb).focus(); - } - isInputFocused = (thumb: Thumb): boolean => { - return this._delegate._getInput(thumb)._isFocused(); - } - getThumbKnobWidth = (thumb: Thumb): number => { - // TODO(wagnermaciel): Check if this causes issues for SSR - // once the mdc-slider is added back to the kitchen sink SSR app. - return this._delegate._getKnobElement(thumb).getBoundingClientRect().width; - } - getThumbBoundingClientRect = (thumb: Thumb): ClientRect => { - return this._delegate._getThumbElement(thumb).getBoundingClientRect(); - } - getBoundingClientRect = (): ClientRect => { - return this._delegate._elementRef.nativeElement.getBoundingClientRect(); - } - isRTL = (): boolean => { - // TODO(wagnermaciel): Actually implementing this. - return false; - } - setThumbStyleProperty = (propertyName: string, value: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).style.setProperty(propertyName, value); - } - removeThumbStyleProperty = (propertyName: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).style.removeProperty(propertyName); - } - setTrackActiveStyleProperty = (propertyName: string, value: string): void => { - this._delegate._trackActive.nativeElement.style.setProperty(propertyName, value); - } - removeTrackActiveStyleProperty = (propertyName: string): void => { - this._delegate._trackActive.nativeElement.style.removeProperty(propertyName); - } - setValueIndicatorText = (value: number, thumb: Thumb): void => { - this._delegate._setValueIndicatorText(value, thumb); - } - getValueToAriaValueTextFn = (): ((value: number) => string) | null => { - return this._delegate.displayWith; - } - updateTickMarks = (tickMarks: TickMark[]): void => { - this._delegate._tickMarks = tickMarks; - this._delegate._cdr.markForCheck(); - } - setPointerCapture = (pointerId: number): void => { - this._delegate._elementRef.nativeElement.setPointerCapture(pointerId); - } - // We ignore emitChangeEvent and emitInputEvent because the slider inputs - // are already exposed so users can just listen for those events directly themselves. - emitChangeEvent = (value: number, thumb: Thumb): void => {}; - emitInputEvent = (value: number, thumb: Thumb): void => {}; - emitDragStartEvent = (value: number, thumb: Thumb): void => { - const input = this._delegate._getInput(thumb); - input.dragStart.emit({ source: input, value, thumb }); - } - emitDragEndEvent = (value: number, thumb: Thumb): void => { - const input = this._delegate._getInput(thumb); - input.dragEnd.emit({ source: input, value, thumb }); - } - registerEventHandler = - (evtType: K, handler: SpecificEventListener): void => { - this._delegate._elementRef.nativeElement.addEventListener(evtType, handler); - } - deregisterEventHandler = - (evtType: K, handler: SpecificEventListener): void => { - this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler); - } - registerThumbEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getThumbElement(thumb).addEventListener(evtType, handler); - } - deregisterThumbEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getThumbElement(thumb).removeEventListener(evtType, handler); - } - registerInputEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getInputElement(thumb).addEventListener(evtType, handler); - } - deregisterInputEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getInputElement(thumb).removeEventListener(evtType, handler); - } - registerBodyEventHandler = - (evtType: K, handler: SpecificEventListener): void => { - this._delegate._document.body.addEventListener(evtType, handler); - } - deregisterBodyEventHandler = - (evtType: K, handler: SpecificEventListener): void => { - this._delegate._document.body.removeEventListener(evtType, handler); - } - registerWindowEventHandler = - (evtType: K, handler: SpecificEventListener): void => { - this._delegate._window.addEventListener(evtType, handler); - } - deregisterWindowEventHandler = - (evtType: K, handler: SpecificEventListener): void => { - this._delegate._window.removeEventListener(evtType, handler); - } -} diff --git a/src/material-experimental/mdc-slider/slider-interface.ts b/src/material-experimental/mdc-slider/slider-interface.ts deleted file mode 100644 index 4ea1e6d0cf02..000000000000 --- a/src/material-experimental/mdc-slider/slider-interface.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { - ChangeDetectorRef, - ElementRef, - EventEmitter, - InjectionToken, - QueryList, -} from '@angular/core'; -import {Thumb, TickMark} from '@material/slider'; - -/** - * Represents a drag event emitted by the MatSlider component. - */ -interface _MatSliderDragEventInterface { - /** The MatSliderThumb that was interacted with. */ - source: _MatSliderThumbInterface; - - /** The current value of the slider. */ - value: number; - - /** The thumb that was interacted with. */ - thumb: Thumb; -} - -export interface _MatSliderThumbInterface { - /** The current value of this slider input. */ - value: number; - - /** Event emitted when the slider thumb starts being dragged. */ - dragStart: EventEmitter<_MatSliderDragEventInterface>; - - /** Event emitted when the slider thumb stops being dragged. */ - dragEnd: EventEmitter<_MatSliderDragEventInterface>; - - /** Event emitted every time the MatSliderThumb is blurred. */ - _blur: EventEmitter; - - /** Event emitted every time the MatSliderThumb is focused. */ - _focus: EventEmitter; - - /** Indicates which slider thumb this input corresponds to. */ - thumb: Thumb; - - /** A reference to MatSliderThumbs root/host element. */ - _elementRef: ElementRef; - - /** The injected document if available or fallback to the global document reference. */ - _document: Document; - - /** Returns true if this slider input currently has focus. */ - _isFocused: () => boolean; -} - - -/** - * This is a dummy interface that just contains the properties and methods of MatSlider that are - * used by MatSliderThumb. Rather than directly referencing MatSlider, we use this interface when - * defining MAT_SLIDER to avoid a circular dependency between MatSlider and MatSliderThumb. - */ -export interface _MatSliderInterface { - /** The slider thumb(s). */ - _thumbs: QueryList>; - - /** The slider thumb knob(s) */ - _knobs: QueryList>; - - /** The span containing the slider thumb value indicator text */ - _valueIndicatorTextElements: QueryList>; - - /** The active section of the slider track. */ - _trackActive: ElementRef; - - /** The sliders hidden range input(s). */ - _inputs: QueryList<_MatSliderThumbInterface>; - - /** Whether the slider is disabled. */ - disabled: boolean; - - /** Whether the slider displays a numeric value label upon pressing the thumb. */ - discrete: boolean; - - /** Whether the slider displays tick marks along the slider track. */ - showTickMarks: boolean; - - /** The minimum value that the slider can have. */ - min: number; - - /** The maximum value that the slider can have. */ - max: number; - - /** The values at which the thumb will snap. */ - step: number; - - /** - * Function that will be used to format the value before it is displayed - * in the thumb label. Can be used to format very large number in order - * for them to fit into the slider thumb. - */ - displayWith: ((value: number) => string) | null; - - /** Whether the foundation has been initialized. */ - _initialized: boolean; - - /** The injected document if available or fallback to the global document reference. */ - _document: Document; - - /** - * The defaultView of the injected document if - * available or fallback to global window reference. - */ - _window: Window; - - /** Used to keep track of & render the active & inactive tick marks on the slider track. */ - _tickMarks: TickMark[]; - - /** The change detector ref. */ - _cdr: ChangeDetectorRef; - - /** A reference to MatSliders root/host element. */ - _elementRef: ElementRef; - - /** Sets the value of a slider thumb. */ - _setValue: (value: number, thumb: Thumb) => void; - - /** Whether this is a ranged slider. */ - _isRange: () => boolean; - - /** Gets the slider thumb input of the given thumb. */ - _getInput: (thumb: Thumb) => _MatSliderThumbInterface; - - /** Gets the slider thumb HTML input element of the given thumb. */ - _getInputElement: (thumb: Thumb) => HTMLInputElement; - - /** Gets the slider thumb HTML element of the given thumb. */ - _getThumbElement: (thumb: Thumb) => HTMLElement; - - /** Gets the slider knob HTML element of the given thumb. */ - _getKnobElement: (thumb: Thumb) => HTMLElement; - - /** - * Sets the value indicator text of the given thumb using the given value. - * - * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the - * numeric value as a string. - */ - _setValueIndicatorText: (value: number, thumb: Thumb) => void; - - /** Determines the class name for a HTML element. */ - _getTickMarkClass: (tickMark: TickMark) => string; - - /** Returns an array of the thumb types that exist on the current slider instance. */ - _getThumbTypes: () => Thumb[]; -} - -/** - * Injection token that can be used to inject instances of MatSlider. - */ -export const MAT_SLIDER = new InjectionToken<_MatSliderInterface>('MatSlider'); diff --git a/src/material-experimental/mdc-slider/slider-thumb.ts b/src/material-experimental/mdc-slider/slider-thumb.ts deleted file mode 100644 index a43a5dc7cc01..000000000000 --- a/src/material-experimental/mdc-slider/slider-thumb.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; -import {DOCUMENT} from '@angular/common'; -import { - AfterViewInit, - Directive, - ElementRef, - EventEmitter, - Inject, - Input, - Output, -} from '@angular/core'; -import {Thumb} from '@material/slider'; -import {_MatSliderInterface, MAT_SLIDER} from './slider-interface'; - -/** - * Represents a drag event emitted by the MatSlider component. - */ -export interface MatSliderDragEvent { - /** The MatSliderThumb that was interacted with. */ - source: MatSliderThumb; - - /** The current value of the slider. */ - value: number; - - /** The thumb that was interacted with. */ - thumb: Thumb; -} - -/** - * The native input used by the MatSlider. - */ -@Directive({ - selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]', - host: { - 'class': 'mdc-slider__input', - 'type': 'range', - '(blur)': '_blur.emit()', - '(focus)': '_focus.emit()', - }, -}) -export class MatSliderThumb implements AfterViewInit { - - // ** IMPORTANT NOTE ** - // - // The way `value` is implemented for MatSliderThumb goes against our standard practice. Normally - // we would define a private variable `_value` as the source of truth for the value of the slider - // thumb input. The source of truth for the value of the slider inputs has already been decided - // for us by MDC to be the value attribute on the slider thumb inputs. This is because the MDC - // foundation and adapter expect that the value attribute is the source of truth for the slider - // inputs. - // - // Also, note that the value attribute is completely disconnected from the value property. - - /** The current value of this slider input. */ - @Input() - get value(): number { - return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value')); - } - set value(v: number) { - const value = coerceNumberProperty(v); - - // If the foundation has already been initialized, we need to - // relay any value updates to it so that it can update the UI. - if (this._slider._initialized) { - this._slider._setValue(value, this.thumb); - } else { - // Setup for the MDC foundation. - this._elementRef.nativeElement.setAttribute('value', `${value}`); - } - } - - /** Event emitted when the slider thumb starts being dragged. */ - @Output() readonly dragStart: EventEmitter - = new EventEmitter(); - - /** Event emitted when the slider thumb stops being dragged. */ - @Output() readonly dragEnd: EventEmitter - = new EventEmitter(); - - /** Event emitted every time the MatSliderThumb is blurred. */ - @Output() readonly _blur: EventEmitter = new EventEmitter(); - - /** Event emitted every time the MatSliderThumb is focused. */ - @Output() readonly _focus: EventEmitter = new EventEmitter(); - - /** Indicates which slider thumb this input corresponds to. */ - thumb: Thumb; - - private _document: Document; - - constructor( - @Inject(DOCUMENT) document: any, - @Inject(MAT_SLIDER) private readonly _slider: _MatSliderInterface, - readonly _elementRef: ElementRef, - ) { - this._document = document; - this.thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') - ? Thumb.START - : Thumb.END; - - // Only set the default value if an initial value has not already been provided. - // Note that we are only setting the value attribute at this point. We cannot set the value - // property yet because the min and max have not been set. - if (!_elementRef.nativeElement.hasAttribute('value')) { - this.value = _elementRef.nativeElement.hasAttribute('matSliderEndThumb') - ? _slider.max - : _slider.min; - } - } - - ngAfterViewInit() { - const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') - ? this._slider._getInput(Thumb.START).value - : this._slider.min; - const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') - ? this._slider._getInput(Thumb.END).value - : this._slider.max; - this._elementRef.nativeElement.min = `${min}`; - this._elementRef.nativeElement.max = `${max}`; - - // We can now set the property value because the min and max have now been set. - this._elementRef.nativeElement.value = `${this.value}`; - - // Setup for the MDC foundation. - if (this._slider.disabled) { - this._elementRef.nativeElement.disabled = true; - } - } - - /** Returns true if this slider input currently has focus. */ - _isFocused(): boolean { - return this._document.activeElement === this._elementRef.nativeElement; - } - - static ngAcceptInputType_value: NumberInput; -} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index a6489787472b..0c6afd5f9f8b 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -20,19 +20,146 @@ import { ChangeDetectorRef, Component, ContentChildren, + Directive, ElementRef, + EventEmitter, Inject, Input, OnDestroy, + Output, QueryList, ViewChild, ViewChildren, ViewEncapsulation, } from '@angular/core'; -import {MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; -import {SliderAdapter} from './slider-adapter'; -import {MatSliderThumb} from './slider-thumb'; -import {_MatSliderInterface, _MatSliderThumbInterface, MAT_SLIDER} from './slider-interface'; +import {SpecificEventListener, EventType} from '@material/base'; +import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; + +/** + * Represents a drag event emitted by the MatSlider component. + */ +export interface MatSliderDragEvent { + /** The MatSliderThumb that was interacted with. */ + source: MatSliderThumb; + + /** The MatSlider that was interacted with. */ + parent: MatSlider; + + /** The current value of the slider. */ + value: number; + + /** The thumb that was interacted with. */ + thumb: Thumb; +} + +/** + * The native input used by the MatSlider. + */ +@Directive({ + selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]', + host: { + 'class': 'mdc-slider__input', + 'type': 'range', + '(blur)': '_blur.emit()', + '(focus)': '_focus.emit()', + }, +}) +export class MatSliderThumb implements AfterViewInit { + + // ** IMPORTANT NOTE ** + // + // The way `value` is implemented for MatSliderThumb goes against our standard practice. Normally + // we would define a private variable `_value` as the source of truth for the value of the slider + // thumb input. The source of truth for the value of the slider inputs has already been decided + // for us by MDC to be the value attribute on the slider thumb inputs. This is because the MDC + // foundation and adapter expect that the value attribute is the source of truth for the slider + // inputs. + // + // Also, note that the value attribute is completely disconnected from the value property. + + /** The current value of this slider input. */ + @Input() + get value(): number { + return coerceNumberProperty(this._elementRef.nativeElement.getAttribute('value')); + } + set value(v: number) { + const value = coerceNumberProperty(v); + + // If the foundation has already been initialized, we need to + // relay any value updates to it so that it can update the UI. + if (this._slider._initialized) { + this._slider._setValue(value, this.thumb); + } else { + // Setup for the MDC foundation. + this._elementRef.nativeElement.setAttribute('value', `${value}`); + } + } + + /** Event emitted when the slider thumb starts being dragged. */ + @Output() readonly dragStart: EventEmitter + = new EventEmitter(); + + /** Event emitted when the slider thumb stops being dragged. */ + @Output() readonly dragEnd: EventEmitter + = new EventEmitter(); + + /** Event emitted every time the MatSliderThumb is blurred. */ + @Output() readonly _blur: EventEmitter = new EventEmitter(); + + /** Event emitted every time the MatSliderThumb is focused. */ + @Output() readonly _focus: EventEmitter = new EventEmitter(); + + /** Indicates which slider thumb this input corresponds to. */ + thumb: Thumb; + + private _document: Document; + + constructor( + @Inject(DOCUMENT) document: any, + private readonly _slider: MatSlider, + readonly _elementRef: ElementRef, + ) { + this._document = document; + this.thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? Thumb.START + : Thumb.END; + + // Only set the default value if an initial value has not already been provided. + // Note that we are only setting the value attribute at this point. We cannot set the value + // property yet because the min and max have not been set. + if (!_elementRef.nativeElement.hasAttribute('value')) { + this.value = _elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? _slider.max + : _slider.min; + } + } + + ngAfterViewInit() { + const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? this._slider._getInput(Thumb.START).value + : this._slider.min; + const max = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? this._slider._getInput(Thumb.END).value + : this._slider.max; + this._elementRef.nativeElement.min = `${min}`; + this._elementRef.nativeElement.max = `${max}`; + + // We can now set the property value because the min and max have now been set. + this._elementRef.nativeElement.value = `${this.value}`; + + // Setup for the MDC foundation. + if (this._slider.disabled) { + this._elementRef.nativeElement.disabled = true; + } + } + + /** Returns true if this slider input currently has focus. */ + _isFocused(): boolean { + return this._document.activeElement === this._elementRef.nativeElement; + } + + static ngAcceptInputType_value: NumberInput; +} /** * Allows users to select from a range of values by moving the slider thumb. It is similar in @@ -52,9 +179,9 @@ import {_MatSliderInterface, _MatSliderThumbInterface, MAT_SLIDER} from './slide exportAs: 'matSlider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - providers: [{provide: MAT_SLIDER, useExisting: MatSlider}], + providers: [], }) -export class MatSlider implements _MatSliderInterface, AfterViewInit, OnDestroy { +export class MatSlider implements AfterViewInit, OnDestroy { /** The slider thumb(s). */ @ViewChildren('thumb') _thumbs: QueryList>; @@ -70,7 +197,7 @@ export class MatSlider implements _MatSliderInterface, AfterViewInit, OnDestroy /** The sliders hidden range input(s). */ @ContentChildren(MatSliderThumb, {descendants: false}) - _inputs: QueryList<_MatSliderThumbInterface>; + _inputs: QueryList; /** Whether the slider is disabled. */ @Input() @@ -196,7 +323,7 @@ export class MatSlider implements _MatSliderInterface, AfterViewInit, OnDestroy } /** Gets the slider thumb input of the given thumb. */ - _getInput(thumb: Thumb): _MatSliderThumbInterface { + _getInput(thumb: Thumb): MatSliderThumb { return thumb === Thumb.END ? this._inputs.last! : this._inputs.first!; } @@ -251,6 +378,147 @@ export class MatSlider implements _MatSliderInterface, AfterViewInit, OnDestroy static ngAcceptInputType_step: NumberInput; } +/** The MDCSliderAdapter implementation. */ +class SliderAdapter implements MDCSliderAdapter { + constructor(private readonly _delegate: MatSlider) {} + + // We manually assign functions instead of using prototype methods because + // MDC clobbers the values otherwise. + // See https://github.com/material-components/material-components-web/pull/6256 + + hasClass = (className: string): boolean => { + return this._delegate._elementRef.nativeElement.classList.contains(className); + } + addClass = (className: string): void => { + this._delegate._elementRef.nativeElement.classList.add(className); + } + removeClass = (className: string): void => { + this._delegate._elementRef.nativeElement.classList.remove(className); + } + getAttribute = (attribute: string): string | null => { + return this._delegate._elementRef.nativeElement.getAttribute(attribute); + } + addThumbClass = (className: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).classList.add(className); + } + removeThumbClass = (className: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).classList.remove(className); + } + getInputValue = (thumb: Thumb): string => { + return this._delegate._getInputElement(thumb).value; + } + setInputValue = (value: string, thumb: Thumb): void => { + this._delegate._getInputElement(thumb).value = value; + } + getInputAttribute = (attribute: string, thumb: Thumb): string | null => { + return this._delegate._getInputElement(thumb).getAttribute(attribute); + } + setInputAttribute = (attribute: string, value: string, thumb: Thumb): void => { + this._delegate._getInputElement(thumb).setAttribute(attribute, value); + } + removeInputAttribute = (attribute: string, thumb: Thumb): void => { + this._delegate._getInputElement(thumb).removeAttribute(attribute); + } + focusInput = (thumb: Thumb): void => { + this._delegate._getInputElement(thumb).focus(); + } + isInputFocused = (thumb: Thumb): boolean => { + return this._delegate._getInput(thumb)._isFocused(); + } + getThumbKnobWidth = (thumb: Thumb): number => { + // TODO(wagnermaciel): Check if this causes issues for SSR + // once the mdc-slider is added back to the kitchen sink SSR app. + return this._delegate._getKnobElement(thumb).getBoundingClientRect().width; + } + getThumbBoundingClientRect = (thumb: Thumb): ClientRect => { + return this._delegate._getThumbElement(thumb).getBoundingClientRect(); + } + getBoundingClientRect = (): ClientRect => { + return this._delegate._elementRef.nativeElement.getBoundingClientRect(); + } + isRTL = (): boolean => { + // TODO(wagnermaciel): Actually implementing this. + return false; + } + setThumbStyleProperty = (propertyName: string, value: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).style.setProperty(propertyName, value); + } + removeThumbStyleProperty = (propertyName: string, thumb: Thumb): void => { + this._delegate._getThumbElement(thumb).style.removeProperty(propertyName); + } + setTrackActiveStyleProperty = (propertyName: string, value: string): void => { + this._delegate._trackActive.nativeElement.style.setProperty(propertyName, value); + } + removeTrackActiveStyleProperty = (propertyName: string): void => { + this._delegate._trackActive.nativeElement.style.removeProperty(propertyName); + } + setValueIndicatorText = (value: number, thumb: Thumb): void => { + this._delegate._setValueIndicatorText(value, thumb); + } + getValueToAriaValueTextFn = (): ((value: number) => string) | null => { + return this._delegate.displayWith; + } + updateTickMarks = (tickMarks: TickMark[]): void => { + this._delegate._tickMarks = tickMarks; + this._delegate._cdr.markForCheck(); + } + setPointerCapture = (pointerId: number): void => { + this._delegate._elementRef.nativeElement.setPointerCapture(pointerId); + } + // We ignore emitChangeEvent and emitInputEvent because the slider inputs + // are already exposed so users can just listen for those events directly themselves. + emitChangeEvent = (value: number, thumb: Thumb): void => {}; + emitInputEvent = (value: number, thumb: Thumb): void => {}; + emitDragStartEvent = (value: number, thumb: Thumb): void => { + const input = this._delegate._getInput(thumb); + input.dragStart.emit({ source: input, parent: this._delegate, value, thumb }); + } + emitDragEndEvent = (value: number, thumb: Thumb): void => { + const input = this._delegate._getInput(thumb); + input.dragEnd.emit({ source: input, parent: this._delegate, value, thumb }); + } + registerEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._elementRef.nativeElement.addEventListener(evtType, handler); + } + deregisterEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler); + } + registerThumbEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumb).addEventListener(evtType, handler); + } + deregisterThumbEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumb).removeEventListener(evtType, handler); + } + registerInputEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumb).addEventListener(evtType, handler); + } + deregisterInputEventHandler = + (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumb).removeEventListener(evtType, handler); + } + registerBodyEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._document.body.addEventListener(evtType, handler); + } + deregisterBodyEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._document.body.removeEventListener(evtType, handler); + } + registerWindowEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._window.addEventListener(evtType, handler); + } + deregisterWindowEventHandler = + (evtType: K, handler: SpecificEventListener): void => { + this._delegate._window.removeEventListener(evtType, handler); + } +} + /** * Ensures that there is not an invalid configuration for the slider thumb inputs. */ From 6e8ab85e128825d3febfb0365e657e58f275fb74 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Tue, 16 Feb 2021 15:38:07 -0800 Subject: [PATCH 10/12] fix(material-experimental/mdc-slider): code review changes * delete empty providers * rename thumb to _thumb and make it private in MatSliderThumb * delete thumb property in MatSliderDragEvent --- src/dev-app/mdc-slider/mdc-slider-demo.html | 4 ++++ src/material-experimental/mdc-slider/slider.scss | 6 ++++++ src/material-experimental/mdc-slider/slider.ts | 14 +++++--------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/dev-app/mdc-slider/mdc-slider-demo.html b/src/dev-app/mdc-slider/mdc-slider-demo.html index e69de29bb2d1..7d170e878b0b 100644 --- a/src/dev-app/mdc-slider/mdc-slider-demo.html +++ b/src/dev-app/mdc-slider/mdc-slider-demo.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/material-experimental/mdc-slider/slider.scss b/src/material-experimental/mdc-slider/slider.scss index e69de29bb2d1..69598322a756 100644 --- a/src/material-experimental/mdc-slider/slider.scss +++ b/src/material-experimental/mdc-slider/slider.scss @@ -0,0 +1,6 @@ +@import '@material/slider/slider'; +@include core-styles; + +mat-slider { + display: block; +} diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 0c6afd5f9f8b..6647415dccce 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -47,9 +47,6 @@ export interface MatSliderDragEvent { /** The current value of the slider. */ value: number; - - /** The thumb that was interacted with. */ - thumb: Thumb; } /** @@ -88,7 +85,7 @@ export class MatSliderThumb implements AfterViewInit { // If the foundation has already been initialized, we need to // relay any value updates to it so that it can update the UI. if (this._slider._initialized) { - this._slider._setValue(value, this.thumb); + this._slider._setValue(value, this._thumb); } else { // Setup for the MDC foundation. this._elementRef.nativeElement.setAttribute('value', `${value}`); @@ -110,7 +107,7 @@ export class MatSliderThumb implements AfterViewInit { @Output() readonly _focus: EventEmitter = new EventEmitter(); /** Indicates which slider thumb this input corresponds to. */ - thumb: Thumb; + private _thumb: Thumb; private _document: Document; @@ -120,7 +117,7 @@ export class MatSliderThumb implements AfterViewInit { readonly _elementRef: ElementRef, ) { this._document = document; - this.thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') + this._thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') ? Thumb.START : Thumb.END; @@ -179,7 +176,6 @@ export class MatSliderThumb implements AfterViewInit { exportAs: 'matSlider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - providers: [], }) export class MatSlider implements AfterViewInit, OnDestroy { /** The slider thumb(s). */ @@ -471,11 +467,11 @@ class SliderAdapter implements MDCSliderAdapter { emitInputEvent = (value: number, thumb: Thumb): void => {}; emitDragStartEvent = (value: number, thumb: Thumb): void => { const input = this._delegate._getInput(thumb); - input.dragStart.emit({ source: input, parent: this._delegate, value, thumb }); + input.dragStart.emit({ source: input, parent: this._delegate, value }); } emitDragEndEvent = (value: number, thumb: Thumb): void => { const input = this._delegate._getInput(thumb); - input.dragEnd.emit({ source: input, parent: this._delegate, value, thumb }); + input.dragEnd.emit({ source: input, parent: this._delegate, value }); } registerEventHandler = (evtType: K, handler: SpecificEventListener): void => { From 3099b97e278dfac546f2170c23a7e48add340b0b Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Wed, 17 Feb 2021 10:13:59 -0800 Subject: [PATCH 11/12] fix(material-experimental/mdc-slider): code review changes * revert to using a method to display the value indicator text instead of directly through the DOM * avoid styling based on tag name and instead target class names applied to host elements * fix indentation in slider.scss * condense jsdoc to oneline * expand MatSliderThumb jsdoc * reworded comment in MatSliderThumb * move initialization of _thumb value to property definition * move input value attribute initialization to separate method * move input min and max property initialization to separate method * move input value property initialization to separate method * remove unnecessary ! --- .../mdc-slider/slider.html | 2 +- .../mdc-slider/slider.scss | 4 +- .../mdc-slider/slider.ts | 123 ++++++++++++------ 3 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.html b/src/material-experimental/mdc-slider/slider.html index 2028d4c4affe..b8f42c85be02 100644 --- a/src/material-experimental/mdc-slider/slider.html +++ b/src/material-experimental/mdc-slider/slider.html @@ -16,7 +16,7 @@
- + {{_getValueIndicatorText(thumb)}}
diff --git a/src/material-experimental/mdc-slider/slider.scss b/src/material-experimental/mdc-slider/slider.scss index 69598322a756..edbdb0402de2 100644 --- a/src/material-experimental/mdc-slider/slider.scss +++ b/src/material-experimental/mdc-slider/slider.scss @@ -1,6 +1,6 @@ @import '@material/slider/slider'; @include core-styles; -mat-slider { - display: block; +.mdc-slider { + display: block; } diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 6647415dccce..138d1f644e90 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -35,9 +35,7 @@ import { import {SpecificEventListener, EventType} from '@material/base'; import {MDCSliderAdapter, MDCSliderFoundation, Thumb, TickMark} from '@material/slider'; -/** - * Represents a drag event emitted by the MatSlider component. - */ +/** Represents a drag event emitted by the MatSlider component. */ export interface MatSliderDragEvent { /** The MatSliderThumb that was interacted with. */ source: MatSliderThumb; @@ -50,7 +48,12 @@ export interface MatSliderDragEvent { } /** - * The native input used by the MatSlider. + * Directive that adds slider-specific behaviors to an input element inside ``. + * Up to two may be placed inside of a ``. + * + * If one is used, the selector `matSliderThumb` must be used, and the outcome will be a normal + * slider. If two are used, the selectors `matSliderStartThumb` and `matSliderEndThumb` must be + * used, and the outcome will be a range slider with two slider thumbs. */ @Directive({ selector: 'input[matSliderThumb], input[matSliderStartThumb], input[matSliderEndThumb]', @@ -65,12 +68,12 @@ export class MatSliderThumb implements AfterViewInit { // ** IMPORTANT NOTE ** // - // The way `value` is implemented for MatSliderThumb goes against our standard practice. Normally - // we would define a private variable `_value` as the source of truth for the value of the slider - // thumb input. The source of truth for the value of the slider inputs has already been decided - // for us by MDC to be the value attribute on the slider thumb inputs. This is because the MDC - // foundation and adapter expect that the value attribute is the source of truth for the slider - // inputs. + // The way `value` is implemented for MatSliderThumb doesn't follow typical Angular conventions. + // Normally we would define a private variable `_value` as the source of truth for the value of + // the slider thumb input. The source of truth for the value of the slider inputs has already + // been decided for us by MDC to be the value attribute on the slider thumb inputs. This is + // because the MDC foundation and adapter expect that the value attribute is the source of truth + // for the slider inputs. // // Also, note that the value attribute is completely disconnected from the value property. @@ -107,7 +110,9 @@ export class MatSliderThumb implements AfterViewInit { @Output() readonly _focus: EventEmitter = new EventEmitter(); /** Indicates which slider thumb this input corresponds to. */ - private _thumb: Thumb; + private _thumb: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + ? Thumb.START + : Thumb.END; private _document: Document; @@ -117,21 +122,40 @@ export class MatSliderThumb implements AfterViewInit { readonly _elementRef: ElementRef, ) { this._document = document; - this._thumb = _elementRef.nativeElement.hasAttribute('matSliderStartThumb') - ? Thumb.START - : Thumb.END; - - // Only set the default value if an initial value has not already been provided. - // Note that we are only setting the value attribute at this point. We cannot set the value - // property yet because the min and max have not been set. - if (!_elementRef.nativeElement.hasAttribute('value')) { - this.value = _elementRef.nativeElement.hasAttribute('matSliderEndThumb') - ? _slider.max - : _slider.min; - } + // By calling this in the constructor we guarantee that the sibling sliders initial value by + // has already been set by the time we reach ngAfterViewInit(). + this._initializeInputValueAttribute(); } ngAfterViewInit() { + this._initializeInputMinMax(); + this._initializeInputValueProperty(); + + // Setup for the MDC foundation. + if (this._slider.disabled) { + this._elementRef.nativeElement.disabled = true; + } + } + + /** Returns true if this slider input currently has focus. */ + _isFocused(): boolean { + return this._document.activeElement === this._elementRef.nativeElement; + } + + /** + * Sets the min and max properties on the slider thumb input. + * + * Must be called AFTER the sibling slider thumb input is guaranteed to have had its value + * attribute value set. For a range slider, the min and max of the slider thumb input depends on + * the value of its sibling slider thumb inputs value. + * + * Must be called BEFORE the value property is set. In the case where the min and max have not + * yet been set and we are setting the input value property to a value outside of the native + * inputs default min or max. The value property would not be set to our desired value, but + * instead be capped at either the default min or max. + * + */ + private _initializeInputMinMax(): void { const min = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') ? this._slider._getInput(Thumb.START).value : this._slider.min; @@ -140,19 +164,33 @@ export class MatSliderThumb implements AfterViewInit { : this._slider.max; this._elementRef.nativeElement.min = `${min}`; this._elementRef.nativeElement.max = `${max}`; + } - // We can now set the property value because the min and max have now been set. + /** + * Sets the value property on the slider thumb input. + * + * Must be called AFTER the min and max have been set. In the case where the min and max have not + * yet been set and we are setting the input value property to a value outside of the native + * inputs default min or max. The value property would not be set to our desired value, but + * instead be capped at either the default min or max. + */ + private _initializeInputValueProperty(): void { this._elementRef.nativeElement.value = `${this.value}`; - - // Setup for the MDC foundation. - if (this._slider.disabled) { - this._elementRef.nativeElement.disabled = true; - } } - /** Returns true if this slider input currently has focus. */ - _isFocused(): boolean { - return this._document.activeElement === this._elementRef.nativeElement; + /** + * Ensures the value attribute is initialized. + * + * Must be called BEFORE the min and max are set. For a range slider, the min and max of the + * slider thumb input depends on the value of its sibling slider thumb inputs value. + */ + private _initializeInputValueAttribute(): void { + // Only set the default value if an initial value has not already been provided. + if (!this._elementRef.nativeElement.hasAttribute('value')) { + this.value = this._elementRef.nativeElement.hasAttribute('matSliderEndThumb') + ? this._slider.max + : this._slider.min; + } } static ngAcceptInputType_value: NumberInput; @@ -265,6 +303,12 @@ export class MatSlider implements AfterViewInit, OnDestroy { /** Used to keep track of & render the active & inactive tick marks on the slider track. */ _tickMarks: TickMark[]; + /** The display value of the start thumb. */ + private _startValueIndicatorText: string; + + /** The display value of the end thumb. */ + private _endValueIndicatorText: string; + constructor( readonly _cdr: ChangeDetectorRef, readonly _elementRef: ElementRef, @@ -340,18 +384,21 @@ export class MatSlider implements AfterViewInit, OnDestroy { return knobElementRef.nativeElement; } + _getValueIndicatorText(thumb: Thumb) { + return thumb === Thumb.START ? this._startValueIndicatorText : this._endValueIndicatorText; + } + /** * Sets the value indicator text of the given thumb using the given value. * * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the * numeric value as a string. */ - _setValueIndicatorText(value: number, thumb: Thumb): void { - const valueIndicatorTextElementRef = thumb === Thumb.END - ? this._valueIndicatorTextElements.last - : this._valueIndicatorTextElements.first; + _setValueIndicatorText(value: number, thumb: Thumb) { const valueText = this.displayWith ? this.displayWith(value) : `${value}`; - valueIndicatorTextElementRef.nativeElement.textContent = valueText; + thumb === Thumb.START + ? this._startValueIndicatorText = valueText + : this._endValueIndicatorText = valueText; } /** Determines the class name for a HTML element. */ @@ -523,7 +570,7 @@ function _validateInputs( startInputElement: HTMLInputElement, endInputElement: HTMLInputElement): void { if (isRange) { - if (!startInputElement!.hasAttribute('matSliderStartThumb')) { + if (!startInputElement.hasAttribute('matSliderStartThumb')) { _throwInvalidInputConfigurationError(); } if (!endInputElement.hasAttribute('matSliderEndThumb')) { From 0558d5bc788ae288b4ba0bbbf55ce1cc383a89e0 Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Wed, 17 Feb 2021 11:56:38 -0800 Subject: [PATCH 12/12] fix(material-experimental/mdc-slider): use thumbPosition instead of thumb --- .../mdc-slider/slider.ts | 136 +++++++++--------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 138d1f644e90..5283903f42c2 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -88,7 +88,7 @@ export class MatSliderThumb implements AfterViewInit { // If the foundation has already been initialized, we need to // relay any value updates to it so that it can update the UI. if (this._slider._initialized) { - this._slider._setValue(value, this._thumb); + this._slider._setValue(value, this._thumbPosition); } else { // Setup for the MDC foundation. this._elementRef.nativeElement.setAttribute('value', `${value}`); @@ -110,7 +110,7 @@ export class MatSliderThumb implements AfterViewInit { @Output() readonly _focus: EventEmitter = new EventEmitter(); /** Indicates which slider thumb this input corresponds to. */ - private _thumb: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') + private _thumbPosition: Thumb = this._elementRef.nativeElement.hasAttribute('matSliderStartThumb') ? Thumb.START : Thumb.END; @@ -351,8 +351,8 @@ export class MatSlider implements AfterViewInit, OnDestroy { } /** Sets the value of a slider thumb. */ - _setValue(value: number, thumb: Thumb): void { - thumb === Thumb.START + _setValue(value: number, thumbPosition: Thumb): void { + thumbPosition === Thumb.START ? this._foundation.setValueStart(value) : this._foundation.setValue(value); } @@ -362,41 +362,43 @@ export class MatSlider implements AfterViewInit, OnDestroy { return this._inputs.length === 2; } - /** Gets the slider thumb input of the given thumb. */ - _getInput(thumb: Thumb): MatSliderThumb { - return thumb === Thumb.END ? this._inputs.last! : this._inputs.first!; + /** Gets the slider thumb input of the given thumb position. */ + _getInput(thumbPosition: Thumb): MatSliderThumb { + return thumbPosition === Thumb.END ? this._inputs.last! : this._inputs.first!; } - /** Gets the slider thumb HTML input element of the given thumb. */ - _getInputElement(thumb: Thumb): HTMLInputElement { - return this._getInput(thumb)._elementRef.nativeElement; + /** Gets the slider thumb HTML input element of the given thumb position. */ + _getInputElement(thumbPosition: Thumb): HTMLInputElement { + return this._getInput(thumbPosition)._elementRef.nativeElement; } - /** Gets the slider thumb HTML element of the given thumb. */ - _getThumbElement(thumb: Thumb): HTMLElement { - const thumbElementRef = thumb === Thumb.END ? this._thumbs.last : this._thumbs.first; + /** Gets the slider thumb HTML element of the given thumb position. */ + _getThumbElement(thumbPosition: Thumb): HTMLElement { + const thumbElementRef = thumbPosition === Thumb.END ? this._thumbs.last : this._thumbs.first; return thumbElementRef.nativeElement; } - /** Gets the slider knob HTML element of the given thumb. */ - _getKnobElement(thumb: Thumb): HTMLElement { - const knobElementRef = thumb === Thumb.END ? this._knobs.last : this._knobs.first; + /** Gets the slider knob HTML element of the given thumb position. */ + _getKnobElement(thumbPosition: Thumb): HTMLElement { + const knobElementRef = thumbPosition === Thumb.END ? this._knobs.last : this._knobs.first; return knobElementRef.nativeElement; } - _getValueIndicatorText(thumb: Thumb) { - return thumb === Thumb.START ? this._startValueIndicatorText : this._endValueIndicatorText; + _getValueIndicatorText(thumbPosition: Thumb) { + return thumbPosition === Thumb.START + ? this._startValueIndicatorText + : this._endValueIndicatorText; } /** - * Sets the value indicator text of the given thumb using the given value. + * Sets the value indicator text of the given thumb position using the given value. * * Uses the `displayWith` function if one has been provided. Otherwise, it just uses the * numeric value as a string. */ - _setValueIndicatorText(value: number, thumb: Thumb) { + _setValueIndicatorText(value: number, thumbPosition: Thumb) { const valueText = this.displayWith ? this.displayWith(value) : `${value}`; - thumb === Thumb.START + thumbPosition === Thumb.START ? this._startValueIndicatorText = valueText : this._endValueIndicatorText = valueText; } @@ -441,40 +443,40 @@ class SliderAdapter implements MDCSliderAdapter { getAttribute = (attribute: string): string | null => { return this._delegate._elementRef.nativeElement.getAttribute(attribute); } - addThumbClass = (className: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).classList.add(className); + addThumbClass = (className: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).classList.add(className); } - removeThumbClass = (className: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).classList.remove(className); + removeThumbClass = (className: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).classList.remove(className); } - getInputValue = (thumb: Thumb): string => { - return this._delegate._getInputElement(thumb).value; + getInputValue = (thumbPosition: Thumb): string => { + return this._delegate._getInputElement(thumbPosition).value; } - setInputValue = (value: string, thumb: Thumb): void => { - this._delegate._getInputElement(thumb).value = value; + setInputValue = (value: string, thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).value = value; } - getInputAttribute = (attribute: string, thumb: Thumb): string | null => { - return this._delegate._getInputElement(thumb).getAttribute(attribute); + getInputAttribute = (attribute: string, thumbPosition: Thumb): string | null => { + return this._delegate._getInputElement(thumbPosition).getAttribute(attribute); } - setInputAttribute = (attribute: string, value: string, thumb: Thumb): void => { - this._delegate._getInputElement(thumb).setAttribute(attribute, value); + setInputAttribute = (attribute: string, value: string, thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).setAttribute(attribute, value); } - removeInputAttribute = (attribute: string, thumb: Thumb): void => { - this._delegate._getInputElement(thumb).removeAttribute(attribute); + removeInputAttribute = (attribute: string, thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).removeAttribute(attribute); } - focusInput = (thumb: Thumb): void => { - this._delegate._getInputElement(thumb).focus(); + focusInput = (thumbPosition: Thumb): void => { + this._delegate._getInputElement(thumbPosition).focus(); } - isInputFocused = (thumb: Thumb): boolean => { - return this._delegate._getInput(thumb)._isFocused(); + isInputFocused = (thumbPosition: Thumb): boolean => { + return this._delegate._getInput(thumbPosition)._isFocused(); } - getThumbKnobWidth = (thumb: Thumb): number => { + getThumbKnobWidth = (thumbPosition: Thumb): number => { // TODO(wagnermaciel): Check if this causes issues for SSR // once the mdc-slider is added back to the kitchen sink SSR app. - return this._delegate._getKnobElement(thumb).getBoundingClientRect().width; + return this._delegate._getKnobElement(thumbPosition).getBoundingClientRect().width; } - getThumbBoundingClientRect = (thumb: Thumb): ClientRect => { - return this._delegate._getThumbElement(thumb).getBoundingClientRect(); + getThumbBoundingClientRect = (thumbPosition: Thumb): ClientRect => { + return this._delegate._getThumbElement(thumbPosition).getBoundingClientRect(); } getBoundingClientRect = (): ClientRect => { return this._delegate._elementRef.nativeElement.getBoundingClientRect(); @@ -483,11 +485,11 @@ class SliderAdapter implements MDCSliderAdapter { // TODO(wagnermaciel): Actually implementing this. return false; } - setThumbStyleProperty = (propertyName: string, value: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).style.setProperty(propertyName, value); + setThumbStyleProperty = (propertyName: string, value: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).style.setProperty(propertyName, value); } - removeThumbStyleProperty = (propertyName: string, thumb: Thumb): void => { - this._delegate._getThumbElement(thumb).style.removeProperty(propertyName); + removeThumbStyleProperty = (propertyName: string, thumbPosition: Thumb): void => { + this._delegate._getThumbElement(thumbPosition).style.removeProperty(propertyName); } setTrackActiveStyleProperty = (propertyName: string, value: string): void => { this._delegate._trackActive.nativeElement.style.setProperty(propertyName, value); @@ -495,8 +497,8 @@ class SliderAdapter implements MDCSliderAdapter { removeTrackActiveStyleProperty = (propertyName: string): void => { this._delegate._trackActive.nativeElement.style.removeProperty(propertyName); } - setValueIndicatorText = (value: number, thumb: Thumb): void => { - this._delegate._setValueIndicatorText(value, thumb); + setValueIndicatorText = (value: number, thumbPosition: Thumb): void => { + this._delegate._setValueIndicatorText(value, thumbPosition); } getValueToAriaValueTextFn = (): ((value: number) => string) | null => { return this._delegate.displayWith; @@ -510,14 +512,14 @@ class SliderAdapter implements MDCSliderAdapter { } // We ignore emitChangeEvent and emitInputEvent because the slider inputs // are already exposed so users can just listen for those events directly themselves. - emitChangeEvent = (value: number, thumb: Thumb): void => {}; - emitInputEvent = (value: number, thumb: Thumb): void => {}; - emitDragStartEvent = (value: number, thumb: Thumb): void => { - const input = this._delegate._getInput(thumb); + emitChangeEvent = (value: number, thumbPosition: Thumb): void => {}; + emitInputEvent = (value: number, thumbPosition: Thumb): void => {}; + emitDragStartEvent = (value: number, thumbPosition: Thumb): void => { + const input = this._delegate._getInput(thumbPosition); input.dragStart.emit({ source: input, parent: this._delegate, value }); } - emitDragEndEvent = (value: number, thumb: Thumb): void => { - const input = this._delegate._getInput(thumb); + emitDragEndEvent = (value: number, thumbPosition: Thumb): void => { + const input = this._delegate._getInput(thumbPosition); input.dragEnd.emit({ source: input, parent: this._delegate, value }); } registerEventHandler = @@ -528,21 +530,21 @@ class SliderAdapter implements MDCSliderAdapter { (evtType: K, handler: SpecificEventListener): void => { this._delegate._elementRef.nativeElement.removeEventListener(evtType, handler); } - registerThumbEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getThumbElement(thumb).addEventListener(evtType, handler); + registerThumbEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumbPosition).addEventListener(evtType, handler); } - deregisterThumbEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getThumbElement(thumb).removeEventListener(evtType, handler); + deregisterThumbEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getThumbElement(thumbPosition).removeEventListener(evtType, handler); } - registerInputEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getInputElement(thumb).addEventListener(evtType, handler); + registerInputEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumbPosition).addEventListener(evtType, handler); } - deregisterInputEventHandler = - (thumb: Thumb, evtType: K, handler: SpecificEventListener): void => { - this._delegate._getInputElement(thumb).removeEventListener(evtType, handler); + deregisterInputEventHandler = + (thumbPosition: Thumb, evtType: K, handler: SpecificEventListener): void => { + this._delegate._getInputElement(thumbPosition).removeEventListener(evtType, handler); } registerBodyEventHandler = (evtType: K, handler: SpecificEventListener): void => {