0

I have an input tag like

<input
      #sliderInput
      id="slider"
      [attr.min]="slider.min"
      [attr.Max]="slider.max"
      [attr.step]="slider.stepSize"
      type="range"
      [(ngModel)]="slider.value"
      (input)="onInputSlider()"
      [attr.disabled]="slider.isDisabled ? '' : null"
/>

How to make this input field with non-linear values? I have an array -> [3, 6, 9, 12, 15, 18, 24, 36, 48];

ofsonmez
  • 3
  • 3

2 Answers2

0

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

  1. a "line" gray
  2. a "line" blue
  3. a "indicator"
  4. 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;
        }
      });
  }
Eliseo
  • 50,109
  • 4
  • 29
  • 67
0

Using cdk-drag can be make the things more "easy"

just add two functions:

  evaluatePosition=(posContainer:any,dragRef:any)=>
  {
    const {
      width,
      height
    } = this.indicator.nativeElement.getBoundingClientRect();
    const pos =
            this.values[0] + (this.range * (posContainer.x - this.pos.x)) / this.pos.width;
          const max = this.values[this.values.length - 1];
          const value = this.values.reduce((a, b) => {
            return Math.abs(a - pos) > Math.abs(b - pos) ? b : a;
          }, max);
          const left =
      ((value - this.values[0]) * this.pos.width) / this.range+this.incr.x
      this.renderer.setStyle(
        this.lineBlue.nativeElement,
        'width',((left + this.size.width-width/2)>0?
        left + this.size.width-width/2:0) + 'px'
      );
      const rect = this.container.nativeElement.getBoundingClientRect();
      this.ngZone.run(()=>{
        this.value = value;

      })
    return {x:left+rect.x,y:pos.y};
  }
  initDrag(event:any)
  {
    const rect=this.indicator.nativeElement.getBoundingClientRect()
    this.incr={x:event.clientX-rect.x-rect.width/2,y:event.clientY-rect.y-rect.height/2} 
    this.onDrag=true;
  }

and the .html like

<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 cdkDrag
    #indicator
    class="indicator"
    [style.background]="background"
    (mousedown)=initDrag($event)
    (cdkDragEnded)="onDrag=false"
    [cdkDragConstrainPosition]="evaluatePosition"
    cdkDragLockAxis="x"
  >
    <span
      *ngIf="showValue == 'always' || (onDrag && showValue == 'auto')"
      >{{ value }}</span
    >
  </div>
</div>

see the stackblitz

Eliseo
  • 50,109
  • 4
  • 29
  • 67