File

src/slider/slider.component.ts

Description

Used to select from ranges of values. See here for usage information.

Get started with importing the module:

Example :
import { SliderModule } from 'carbon-components-angular';

The simplest possible slider usage looks something like:

Example :
    <cds-slider></cds-slider>

That will render a slider without labels or alternative value input. Labels can be provided by elements with [minLabel] and [maxLabel] attributes, and an input (may use the ibmInput directive) can be supplied for use as an alternative value field.

ex:

Example :
<!-- Full example -->
<cds-slider>
        <span minLabel>0GB</span>
        <span maxLabel>100GB</span>
        <input/>
</cds-slider>

<!-- with just an input -->
<cds-slider>
        <input/>
</cds-slider>

<!-- with just one label -->
<cds-slider>
        <span maxLabel>Maximum</span>
</cds-slider>

Slider supports NgModel by default, as well as two way binding to the value input.

See demo

Implements

AfterViewInit ControlValueAccessor

Metadata

Index

Properties
Methods
Inputs
Outputs
HostBindings
Accessors

Constructor

constructor(elementRef: ElementRef, eventService: EventService, changeDetection: ChangeDetectorRef)
Parameters :
Name Type Optional
elementRef ElementRef No
eventService EventService No
changeDetection ChangeDetectorRef No

Inputs

disableArrowKeys
Type : boolean
Default value : false

Set to true for a slider without arrow key interactions.

disabled
Type : boolean

Disables the range visually and functionally

id
Type : string
Default value : `slider-${Slider.count++}`

Base ID for the slider. The min and max labels get IDs ${this.id}-bottom-range and ${this.id}-top-range respectively

label
Type : string | TemplateRef<any>

Sets the text inside the label tag

max
Type : number

The upper bound of our range

min
Type : number

The lower bound of our range

readonly
Type : boolean

Set to true for a readonly state.

shiftMultiplier
Type : number
Default value : 4

Value used to "multiply" the step when using arrow keys to select values

skeleton
Type : boolean
Default value : false

Set to true for a loading slider

step
Type : number
Default value : 1

The interval for our range

value
Type : any

Set the initial value. Available for two way binding

Outputs

valueChange
Type : EventEmitter<number | []>

Emits every time a new value is selected

HostBindings

class.cds--form-item
Type : boolean
Default value : true

Methods

convertToPx
convertToPx(value)

Converts a given "real" value to a px value we can update the view with

Parameters :
Name Optional
value No
Returns : any
convertToValue
convertToValue(pxAmount)

Converts a given px value to a "real" value in our range

Parameters :
Name Optional
pxAmount No
Returns : number
decrementValue
decrementValue(multiplier: number, index: number)

Decrements the value by the step value, or the step value multiplied by the multiplier argument.

Parameters :
Name Type Optional Default value
index number No 0
Returns : void
getFractionComplete
getFractionComplete(value: number)

Returns the amount of "completeness" of a value as a fraction of the total track width

Parameters :
Name Type Optional
value number No
Returns : number
Protected getInputs
getInputs()

Get optional input fields

Returns : HTMLInputElement[]
incrementValue
incrementValue(multiplier: number, index: number)

Increments the value by the step value, or the step value multiplied by the multiplier argument.

Parameters :
Name Type Optional Default value
index number No 0
Returns : void
isRange
isRange()

Determines if the slider is in range mode.

Returns : boolean
Public isTemplate
isTemplate(value)
Parameters :
Name Optional
value No
Returns : boolean
ngAfterViewInit
ngAfterViewInit()
Returns : void
onChange
onChange(event, index)

Change handler for the optional input

Parameters :
Name Optional
event No
index No
Returns : void
onClick
onClick(event)

Handles clicks on the slider, and setting the value to it's "real" equivalent. Will assign the value to the closest thumb if in range mode.

Parameters :
Name Optional
event No
Returns : void
onFocus
onFocus(undefined)

Focus handler for the optional input

Parameters :
Name Optional
No
Returns : void
onKeyDown
onKeyDown(event: KeyboardEvent, index: number)

Calls incrementValue for ArrowRight and ArrowUp, decrementValue for ArrowLeft and ArrowDown.

Parameters :
Name Type Optional Default value
event KeyboardEvent No
index number No 0
Returns : void
onMouseDown
onMouseDown(event, index: number)

Enables the onMouseMove handler

Parameters :
Name Type Optional Default value
event No
index number No 0
Returns : void
onMouseMove
onMouseMove(event)

Mouse move handler. Responsible for updating the value and visual selection based on mouse movement

Parameters :
Name Optional
event No
Returns : void
onMouseUp
onMouseUp()

Disables the onMouseMove handler

Returns : void
registerOnChange
registerOnChange(fn: any)

Register a change propagation function for ControlValueAccessor

Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)

Register a callback to notify when our input has been touched

Parameters :
Name Type Optional
fn any No
Returns : void
scaleX
scaleX(complete)

Helper function to return the CSS transform scaleX function

Parameters :
Name Optional
complete No
Returns : string
trackThumbsBy
trackThumbsBy(index: number, item: any)
Parameters :
Name Type Optional
index number No
item any No
Returns : number
updateTrackRangeWidth
updateTrackRangeWidth()

Range mode only. Updates the track width to span from the low thumb to the high thumb

Returns : void
writeValue
writeValue(v: any)

Receives a value from the model

Parameters :
Name Type Optional
v any No
Returns : void

Properties

Protected _disabled
Default value : false
Protected _focusedThumbIndex
Type : number
Default value : 0
Protected _max
Type : number
Default value : 100
Protected _min
Type : number
Default value : 0
Protected _previousValue
Type : []
Default value : [this.min]
Protected _readonly
Default value : false
Protected _value
Type : []
Default value : [this.min]
Public bottomRangeId
Default value : `${this.id}-bottom-range`
Private Static count
Type : number
Default value : 0

Used to generate unique IDs

filledTrack
Type : ElementRef
Decorators :
@ViewChild('filledTrack')
Public fractionComplete
Type : number
Default value : 0
hostClass
Default value : true
Decorators :
@HostBinding('class.cds--form-item')
Protected inputs
Type : HTMLInputElement[]
Protected isMouseDown
Default value : false
Public labelId
Default value : `${this.id}-label`
onTouched
Type : function
Default value : () => {...}

Callback to notify the model when our input has been touched

propagateChange
Default value : () => {...}

Send changes back to the model

range
Type : ElementRef
Decorators :
@ViewChild('range')
thumbs
Type : QueryList<ElementRef>
Decorators :
@ViewChildren('thumbs')
Public topRangeId
Default value : `${this.id}-top-range`
track
Type : ElementRef
Decorators :
@ViewChild('track')

Accessors

min
getmin()
setmin(v)

The lower bound of our range

Parameters :
Name Optional
v No
Returns : void
max
getmax()
setmax(v)

The upper bound of our range

Parameters :
Name Optional
v No
Returns : void
value
getvalue()
setvalue(v)

Set the initial value. Available for two way binding

Parameters :
Name Optional
v No
Returns : void
disabled
getdisabled()
setdisabled(v)

Disables the range visually and functionally

Parameters :
Name Optional
v No
Returns : void
readonly
getreadonly()
setreadonly(v: boolean)

Set to true for a readonly state.

Parameters :
Name Type Optional
v boolean No
Returns : void
import {
	Component,
	HostBinding,
	Input,
	Output,
	EventEmitter,
	AfterViewInit,
	ViewChild,
	ElementRef,
	TemplateRef,
	ViewChildren,
	QueryList,
	ChangeDetectorRef
} from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { EventService } from "carbon-components-angular/utils";

/**
 * Used to select from ranges of values. [See here](https://www.carbondesignsystem.com/components/slider/usage) for usage information.
 *
 * Get started with importing the module:
 *
 * ```typescript
 * import { SliderModule } from 'carbon-components-angular';
 * ```
 *
 * The simplest possible slider usage looks something like:
 *
 * ```html
 *	<cds-slider></cds-slider>
 * ```
 *
 * That will render a slider without labels or alternative value input. Labels can be provided by
 * elements with `[minLabel]` and `[maxLabel]` attributes, and an `input` (may use the `ibmInput` directive) can be supplied
 * for use as an alternative value field.
 *
 * ex:
 *
 * ```html
 * <!-- Full example -->
 * <cds-slider>
 *		<span minLabel>0GB</span>
 *		<span maxLabel>100GB</span>
 *		<input/>
 * </cds-slider>
 *
 * <!-- with just an input -->
 * <cds-slider>
 *		<input/>
 * </cds-slider>
 *
 * <!-- with just one label -->
 * <cds-slider>
 *		<span maxLabel>Maximum</span>
 * </cds-slider>
 * ```
 *
 * Slider supports `NgModel` by default, as well as two way binding to the `value` input.
 *
 * [See demo](../../?path=/story/components-slider--advanced)
 */
@Component({
	selector: "cds-slider, ibm-slider",
	template: `
		<ng-container *ngIf="!skeleton; else skeletonTemplate">
			<label
				*ngIf="label"
				[for]="id"
				[id]="labelId"
				class="cds--label"
				[ngClass]="{'cds--label--disabled': disabled}">
				<ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container>
				<ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label"></ng-template>
			</label>
			<div
				class="cds--slider-container"
				[ngClass]="{ 'cds--slider-container--readonly': readonly }">
				<label [id]="bottomRangeId" class="cds--slider__range-label">
					<ng-content select="[minLabel]"></ng-content>
				</label>
				<div
					class="cds--slider"
					(click)="onClick($event)"
					[ngClass]="{
						'cds--slider--disabled': disabled,
						'cds--slider--readonly': readonly
					}">
					<ng-container *ngIf="!isRange()">
						<div class="cds--slider__thumb-wrapper"
							[ngStyle]="{insetInlineStart: getFractionComplete(value) * 100 + '%'}">
							<div
								#thumbs
								role="slider"
								[id]="id"
								[attr.aria-labelledby]="labelId"
								class="cds--slider__thumb"
								tabindex="0"
								(mousedown)="onMouseDown($event)"
								(keydown)="onKeyDown($event)">
							</div>
						</div>
					</ng-container>
					<ng-container *ngIf="isRange()">
						<div class="cds--slider__thumb-wrapper"
						 [ngStyle]="{insetInlineStart: getFractionComplete(thumb) * 100 + '%'}"
						 *ngFor="let thumb of value; let i = index; trackBy: trackThumbsBy">
							<div
								#thumbs
								role="slider"
								[id]="id + (i > 0 ? '-' + i : '')"
								[attr.aria-labelledby]="labelId"
								class="cds--slider__thumb"
								tabindex="0"
								(mousedown)="onMouseDown($event, i)"
								(keydown)="onKeyDown($event, i)">
							</div>
						</div>
					</ng-container>
					<div
						#track
						class="cds--slider__track">
					</div>
					<div
						#filledTrack
						class="cds--slider__filled-track">
					</div>
					<input
						#range
						aria-label="slider"
						class="cds--slider__input"
						type="range"
						[step]="step"
						[min]="min"
						[max]="max"
						[value]="value.toString()">
				</div>
				<label [id]="topRangeId" class="cds--slider__range-label">
					<ng-content select="[maxLabel]"></ng-content>
				</label>
				<ng-content select="input"></ng-content>
			</div>
		</ng-container>

		<ng-template #skeletonTemplate>
			<label *ngIf="label" class="cds--label cds--skeleton"></label>
			<div class="cds--slider-container cds--skeleton">
				<span class="cds--slider__range-label"></span>
				<div class="cds--slider">
					<div class="cds--slider__thumb"></div>
					<div class="cds--slider__track"></div>
					<div class="cds--slider__filled-track"></div>
				</div>
				<span class="cds--slider__range-label"></span>
			</div>
		</ng-template>
	`,
	providers: [
		{
			provide: NG_VALUE_ACCESSOR,
			useExisting: Slider,
			multi: true
		}
	]
})
export class Slider implements AfterViewInit, ControlValueAccessor {
	/** Used to generate unique IDs */
	private static count = 0;

	/** The lower bound of our range */
	@Input() set min(v) {
		if (!v) { return; }
		this._min = v;
		// force the component to update
		this.value = this.value;
	}
	get min() {
		return this._min;
	}
	/** The upper bound of our range */
	@Input() set max(v) {
		if (!v) { return; }
		this._max = v;
		// force the component to update
		this.value = this.value;
	}

	get max() {
		return this._max;
	}
	/** The interval for our range */
	@Input() step = 1;
	/** Set the initial value. Available for two way binding */
	@Input() set value(v) {
		if (!v) {
			v = [this.min];
		}

		if (typeof v === "number" || typeof v === "string") {
			v = [Number(v)];
		}

		if (v[0] < this.min) {
			v[0] = this.min;
		}

		if (v[0] > this.max) {
			v[0] = this.max;
		}

		if (this.isRange()) {
			if (this._previousValue[0] !== v[0]) { // left moved
				if (v[0] > v[1] - this.step) {
					// stop the left handle if surpassing the right one
					v[0] = v[1] - this.step;
				} else if (v[0] > this.max) {
					v[0] = this.max;
				} else if (v[0] < this.min) {
					v[0] = this.min;
				}
			}

			if (this._previousValue[1] !== v[1]) { // right moved
				if (v[1] > this.max) {
					v[1] = this.max;
				} else if (v[1] < this._value[0] + this.step) {
					// stop the right handle if surpassing the left one
					v[1] = this._value[0] + this.step;
				} else if (v[1] < this.min) {
					v[1] = this.min;
				}
			}
		}

		this._previousValue = [...this._value]; // store a copy, enable detection which handle moved
		this._value = [...v]; // triggers change detection when ngModel value is an array (for range)

		if (this.isRange() && this.filledTrack) {
			this.updateTrackRangeWidth();
		} else if (this.filledTrack) {
			this.filledTrack.nativeElement.style.transform = `translate(0%, -50%) ${this.scaleX(this.getFractionComplete(v[0]))}`;
		}

		if (this.inputs && this.inputs.length) {
			this.inputs.forEach((input, index) => {
				input.value = this._value[index].toString();
			});
		}

		const valueToEmit = this.isRange() ? v : v[0];
		this.propagateChange(valueToEmit);
		this.valueChange.emit(valueToEmit);
	}

	get value() {
		if (this.isRange()) {
			return this._value;
		}
		return this._value[0];
	}

	/** Base ID for the slider. The min and max labels get IDs `${this.id}-bottom-range` and `${this.id}-top-range` respectively */
	@Input() id = `slider-${Slider.count++}`;
	/** Value used to "multiply" the `step` when using arrow keys to select values */
	@Input() shiftMultiplier = 4;
	/** Set to `true` for a loading slider */
	@Input() skeleton = false;
	/** Sets the text inside the `label` tag */
	@Input() label: string | TemplateRef<any>;
	/** Set to `true` for a slider without arrow key interactions. */
	@Input() disableArrowKeys = false;
	/** Disables the range visually and functionally */
	@Input() set disabled(v) {
		this._disabled = v;
		// for some reason `this.input` never exists here, so we have to query for it here too
		const inputs = this.getInputs();
		if (inputs && inputs.length > 0) {
			inputs.forEach(input => input.disabled = v);
		}
	}

	get disabled() {
		return this._disabled;
	}
	/** Set to `true` for a readonly state. */
	@Input() set readonly(v: boolean) {
		this._readonly = v;
		// for some reason `this.input` never exists here, so we have to query for it here too
		const inputs = this.getInputs();
		if (inputs && inputs.length > 0) {
			inputs.forEach(input => input.readOnly = v);
		}
	}
	get readonly() {
		return this._readonly;
	}
	/** Emits every time a new value is selected */
	@Output() valueChange: EventEmitter<number | number[]> = new EventEmitter();
	@HostBinding("class.cds--form-item") hostClass = true;
	@ViewChildren("thumbs") thumbs: QueryList<ElementRef>;

	@ViewChild("track") track: ElementRef;
	@ViewChild("filledTrack") filledTrack: ElementRef;
	@ViewChild("range") range: ElementRef;

	public labelId = `${this.id}-label`;
	public bottomRangeId = `${this.id}-bottom-range`;
	public topRangeId = `${this.id}-top-range`;
	public fractionComplete = 0;

	protected isMouseDown = false;
	protected inputs: HTMLInputElement[];
	protected _min = 0;
	protected _max = 100;
	protected _value = [this.min];
	protected _previousValue = [this.min];
	protected _disabled = false;
	protected _readonly = false;
	protected _focusedThumbIndex = 0;

	constructor(
		protected elementRef: ElementRef,
		protected eventService: EventService,
		private changeDetection: ChangeDetectorRef
	) {}

	ngAfterViewInit() {
		// bind mousemove and mouseup to the document so we don't have issues tracking the mouse
		this.eventService.onDocument("mousemove", this.onMouseMove.bind(this));
		this.eventService.onDocument("mouseup", this.onMouseUp.bind(this));

		// apply any values we got from before the view initialized
		this.changeDetection.detectChanges();

		// TODO: ontouchstart/ontouchmove/ontouchend

		// set up the optional input
		this.inputs = this.getInputs();
		if (this.inputs && this.inputs.length > 0) {
			this.inputs.forEach((input, index) => {
				input.type = "number";
				input.classList.add("cds--slider-text-input");
				input.classList.add("cds--text-input");
				input.setAttribute("aria-labelledby", `${this.bottomRangeId} ${this.topRangeId}`);

				input.value = index < this._value.length ? this._value[index].toString() : this.max.toString();
				// bind events on our optional input
				this.eventService.on(input, "change", event => this.onChange(event, index));

				if (index === 0) {
					this.eventService.on(input, "focus", this.onFocus.bind(this));
				}
			});
		}
	}

	trackThumbsBy(index: number, item: any) {
		return index;
	}

	/** Send changes back to the model */
	propagateChange = (_: any) => { };

	/** Register a change propagation function for `ControlValueAccessor` */
	registerOnChange(fn: any) {
		this.propagateChange = fn;
	}

	/** Callback to notify the model when our input has been touched */
	onTouched: () => any = () => { };

	/** Register a callback to notify when our input has been touched */
	registerOnTouched(fn: any) {
		this.onTouched = fn;
	}

	/** Receives a value from the model */
	writeValue(v: any) {
		this.value = v;
	}

	/**
	 * Returns the amount of "completeness" of a value as a fraction of the total track width
	 */
	getFractionComplete(value: number) {
		if (!this.track) {
			return 0;
		}

		const trackWidth = this.track.nativeElement.getBoundingClientRect().width;
		return this.convertToPx(value) / trackWidth;
	}

	/** Helper function to return the CSS transform `scaleX` function */
	scaleX(complete) {
		return `scaleX(${complete})`;
	}

	/** Converts a given px value to a "real" value in our range */
	convertToValue(pxAmount) {
		// basic concept borrowed from carbon-components
		// https://github.com/carbon-design-system/carbon/blob/43bf3abdc2f8bdaa38aa84e0f733adde1e1e8894/src/components/slider/slider.js#L147-L151
		const range = this.max - this.min;
		const trackWidth = this.track.nativeElement.getBoundingClientRect().width;
		const unrounded = pxAmount / trackWidth;
		const rounded = Math.round((range * unrounded) / this.step) * this.step;
		return rounded + this.min;
	}

	/** Converts a given "real" value to a px value we can update the view with */
	convertToPx(value) {
		if (!this.track) {
			return 0;
		}

		const trackWidth = this.track.nativeElement.getBoundingClientRect().width;
		if (value >= this.max) {
			return trackWidth;
		}

		if (value <= this.min) {
			return 0;
		}

		// account for value shifting by subtracting min from value and max
		return Math.round(trackWidth * ((value - this.min) / (this.max - this.min)));
	}

	/**
	 * Increments the value by the step value, or the step value multiplied by the `multiplier` argument.
	 *
	 * @argument multiplier Defaults to `1`, multiplied with the step value.
	 */
	incrementValue(multiplier = 1, index = 0) {
		this._value[index] = this._value[index] + (this.step * multiplier);
		this.value = this.value; // run the setter
	}

	/**
	 * Decrements the value by the step value, or the step value multiplied by the `multiplier` argument.
	 *
	 * @argument multiplier Defaults to `1`, multiplied with the step value.
	 */
	decrementValue(multiplier = 1, index = 0) {
		this._value[index] = this._value[index] - (this.step * multiplier);
		this.value = this.value; // run the setter
	}

	/**
	 * Determines if the slider is in range mode.
	 */
	isRange(): boolean {
		return this._value.length > 1;
	}

	/**
	 * Range mode only.
	 * Updates the track width to span from the low thumb to the high thumb
	 */
	updateTrackRangeWidth() {
		const fraction = this.getFractionComplete(this._value[0]);
		const fraction2 = this.getFractionComplete(this._value[1]);
		this.filledTrack.nativeElement.style.transform = `translate(${fraction * 100}%, -50%) ${this.scaleX(fraction2 - fraction)}`;
	}

	/** Change handler for the optional input */
	onChange(event, index) {
		this._value[index] = Number(event.target.value);
		this.value = this.value;
	}

	/**
	 * Handles clicks on the slider, and setting the value to it's "real" equivalent.
	 * Will assign the value to the closest thumb if in range mode.
	 * */
	onClick(event) {
		if (this.disabled || this.readonly) { return; }
		const trackLeft = this.track.nativeElement.getBoundingClientRect().left;
		const trackValue = this.convertToValue(event.clientX - trackLeft);
		if (this.isRange()) {
			if (Math.abs(this._value[0] - trackValue) < Math.abs(this._value[1] - trackValue)) {
				this._value[0] = trackValue;
			} else {
				this._value[1] = trackValue;
			}
		} else {
			this._value[0] = trackValue;
		}

		this.value = this.value;
	}

	/** Focus handler for the optional input */
	onFocus({target}) {
		target.select();
	}

	/** Mouse move handler. Responsible for updating the value and visual selection based on mouse movement */
	onMouseMove(event) {
		if (this.disabled || this.readonly || !this.isMouseDown) { return; }
		const track = this.track.nativeElement.getBoundingClientRect();

		let value;

		if (
			event.clientX - track.left <= track.width
			&& event.clientX - track.left >= 0
		) {
			value = this.convertToValue(event.clientX - track.left);
		}

		// if the mouse is beyond the max, set the value to `max`
		if (event.clientX - track.left > track.width) {
			value = this.max;
		}

		// if the mouse is below the min, set the value to `min`
		if (event.clientX - track.left < 0) {
			value = this.min;
		}

		if (value !== undefined) {
			this._value[this._focusedThumbIndex] = value;
			this.value = this.value;
		}
	}

	/**
	 * Enables the `onMouseMove` handler
	 *
	 * @param {boolean} thumb If true then `thumb` is clicked down, otherwise `thumb2` is clicked down.
	 */
	onMouseDown(event, index = 0) {
		event.preventDefault();
		if (this.disabled || this.readonly) { return; }
		this._focusedThumbIndex = index;
		this.thumbs.toArray()[index].nativeElement.focus();
		this.isMouseDown = true;
	}

	/** Disables the `onMouseMove` handler */
	onMouseUp() {
		this.isMouseDown = false;
	}

	/**
	 * Calls `incrementValue` for ArrowRight and ArrowUp, `decrementValue` for ArrowLeft and ArrowDown.
	 *
	 * @param {boolean} thumb If true then `thumb` is pressed down, otherwise `thumb2` is pressed down.
	 */
	onKeyDown(event: KeyboardEvent, index = 0) {
		if (this.disableArrowKeys || this.readonly) {
			return;
		}
		const multiplier = event.shiftKey ? this.shiftMultiplier : 1;
		if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
			this.decrementValue(multiplier, index);
			this.thumbs.toArray()[index].nativeElement.focus();
			event.preventDefault();
		} else if (event.key === "ArrowRight" || event.key === "ArrowUp") {
			this.incrementValue(multiplier, index);
			this.thumbs.toArray()[index].nativeElement.focus();
			event.preventDefault();
		}
	}

	public isTemplate(value) {
		return value instanceof TemplateRef;
	}

	/** Get optional input fields */
	protected getInputs(): HTMLInputElement[] {
		return this.elementRef.nativeElement.querySelectorAll("input:not([type=range])");
	}
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""