I don't know any component that can make you want :(...
but we can create a custom control component :)
Well, the idea is inspirated in this another SO. We listen (click) to subscribe to mouseMove. According the position we change the "style" of one indicator.
Well, In this case I choose use Renderer2 to apply style
See the .html of the component
<div #container class="container">
<div class="line"></div>
<div #line class="line-blue" [style.background]="background"></div>
<ng-container *ngIf="showMarks && values && values.length">
<div
#mark
*ngFor="let value of (values | slice: 1:values.length - 1)"
class="mark"
[style.background]="markBackground"
></div>
</ng-container>
<div
#indicator
class="indicator"
[style.background]="background"
(mousedown)="drag()"
[ngStyle]="style"
>
<span
*ngIf="showValue == 'always' || (onDrag && showValue == 'auto')"
>{{ value }}</span
>
</div>
</div>
See that it's not more a div with position relative where inside has
- a "line" gray
- a "line" blue
- a "indicator"
- a series of "marks" if we want to show the differents steps
We are going to create the function "drag()" that subscribe to mouseMove. As I want this work also in a touch screen, we subscribe also to toucheMove
But before we calculate some variable neccesary in ngAfterViewOnInit. I use ngAfterViewInit to calculate the positions of the "marks"
ngAfterViewInit() {
const {
width,
height
} = this.indicator.nativeElement.getBoundingClientRect();
const rect = this.container.nativeElement.getBoundingClientRect();
this.pos = { x: rect.x, y: rect.y, width: rect.width };
this.size = { width: width / 2, height: height / 2 };
this.marks.forEach((x, index) => {
const left =
((this.values[index + 1] - this.values[0]) * this.pos.width) /
this.range;
this.renderer.setStyle(x.nativeElement, 'left', left + 'px');
});
}
The function "drag" becomes like:
drag() {
this.onDrag = true;
merge(
fromEvent(this.document, 'mousemove'),
fromEvent(this.document, 'touchmove')
)
.pipe(takeWhile(() => this.onDrag))
.subscribe((event: any) => {
const x = event.changedTouches
? event.changedTouches[0].pageX
: event.pageX;
const pos =
this.values[0] + (this.range * (x - this.pos.x)) / this.pos.width;
const max = this.values[this.values.length - 1];
this.value = this.values.reduce((a, b) => {
return Math.abs(a - pos) > Math.abs(b - pos) ? b : a;
}, max);
this.setStyle(this.value);
});
fromEvent(this.document, 'mouseup')
.pipe(take(1))
.subscribe(_ => (this.onDrag = false));
}
//setStyle change the style according the "value"
setStyle(value: number) {
const left = ((value - this.values[0]) * this.pos.width) / this.range - this.size.width;
this.renderer.setStyle(this.indicator.nativeElement, 'left', left + 'px');
this.renderer.setStyle(
this.lineBlue.nativeElement,
'width',
left + this.size.width + 'px'
);
}
Basically, when we received a mouseMove/touchMove we calculate the value more "closer" to the values in the array.
Imagine our component width is 100px, we move to a position 20px and the discrete values are [3, 6, 9, 12, 15, 18, 24, 36, 48]
100px are equivalent to (48-3) value, so 50px are equivalent to 3+(48-3)*50/100=25.5 that is closer to "24". Is the strange reduce
It's interesting use the HostListeners to get the touchedstart
@HostListener('touchstart', ['$event']) _() {
this.drag();
}
@HostListener('touchend', ['$event']) _a() {
this.onDrag = false;
}
To create the custom form control we need implements ControlValueAccesor, that it's only add the properties
onChange = (quantity) => {};
onTouched = () => {};
And the functions:
writeValue(value: number) {
this._value = value;
this.setStyle(value);
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
Only need call to this.onChange when the value change. I like use a getter with "this.value"
get value()
{
return this._value
}
set value(value)
{
this._value=value;
this.onTouched();
this.onChange(value);
}
After add some @Inputs
to allow customizer our control with colors, you can see the result in this stackblitz without Waranties
Update in the stackblitz I improve the code to recalculate the dimensions subscribing to window.resize event and using "map", so when you subscribe received the value, not the position of the mouse. This allow only call to the getter if is different
ngAfterViewInit() {
fromEvent(window, 'resize')
.pipe(startWith(new Event('resize') || null))
.subscribe(() => {
....
})
}
And
drag() {
this.onDrag = true;
merge(
fromEvent(this.document, 'mousemove'),
fromEvent(this.document, 'touchmove')
)
.pipe(
takeWhile(() => this.onDrag),
map((event: any) => {
....
})).subscribe((value: number) => {
if (value!=this.value)
{
this.setStyle(value);
this.value = value;
}
});
}