Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Double sided input slider in React

I have an almost working solution in the sandbox linked in the update below, you should start with this.


I have one that's created without using React (Click "Run Code Snippet")

var thumbsize = 14;

function draw(slider,splitvalue) {

    /* set function vars */
    var min = slider.querySelector('.min');
    var max = slider.querySelector('.max');
    var thumbsize = parseInt(slider.getAttribute('data-thumbsize'));
    var rangewidth = parseInt(slider.getAttribute('data-rangewidth'));
    var rangemin = parseInt(slider.getAttribute('data-rangemin'));
    var rangemax = parseInt(slider.getAttribute('data-rangemax'));

    /* set min and max attributes */
    min.setAttribute('max',splitvalue);
    max.setAttribute('min',splitvalue);

    /* set css */
    min.style.width = parseInt(thumbsize + ((splitvalue - rangemin)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
    max.style.width = parseInt(thumbsize + ((rangemax - splitvalue)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
    min.style.left = '0px';
    max.style.left = parseInt(min.style.width)+'px';
    
    /* correct for 1 off at the end */
    if(max.value>(rangemax - 1)) max.setAttribute('data-value',rangemax);

    /* write value and labels */
    max.value = max.getAttribute('data-value'); 
    min.value = min.getAttribute('data-value');

}

function init(slider) {
    /* set function vars */
    var min = slider.querySelector('.min');
    var max = slider.querySelector('.max');
    var rangemin = parseInt(min.getAttribute('min'));
    var rangemax = parseInt(max.getAttribute('max'));
    var avgvalue = (rangemin + rangemax)/2;

    /* set data-values */
    min.setAttribute('data-value',rangemin);
    max.setAttribute('data-value',rangemax);
    
    /* set data vars */
    slider.setAttribute('data-rangemin',rangemin); 
    slider.setAttribute('data-rangemax',rangemax); 
    slider.setAttribute('data-thumbsize',thumbsize); 
    slider.setAttribute('data-rangewidth',slider.offsetWidth);

    /* draw */
    draw(slider,avgvalue);

    /* events */
    min.addEventListener("input", function() {update(min);});
    max.addEventListener("input", function() {update(max);});
}

function update(el){
    /* set function vars */
    var slider = el.parentElement;
    var min = slider.querySelector('#min');
    var max = slider.querySelector('#max');
    var minvalue = Math.floor(min.value);
    var maxvalue = Math.floor(max.value);
    
    /* set inactive values before draw */
    min.setAttribute('data-value',minvalue);
    max.setAttribute('data-value',maxvalue);

    var avgvalue = (minvalue + maxvalue)/2;

    /* draw */
    draw(slider,avgvalue);
}

var sliders = document.querySelectorAll('.min-max-slider');
sliders.forEach( function(slider) {
    init(slider);
});
.min-max-slider {
  position: relative;
  width: 200px;
  text-align: center;
  margin-bottom: 50px;
 }

.min-max-slider > label {
  display: none;
 }
 
.min-max-slider > input {
  cursor: pointer;
  position: absolute;
}


/* webkit specific styling */
.min-max-slider > input {
  -webkit-appearance: none;
  outline: none!important;
  background: transparent;
  background-image: linear-gradient(to bottom, transparent 0%, transparent 30%, silver 30%, silver 60%, transparent 60%, transparent 100%);
}

.min-max-slider > input::-webkit-slider-thumb {
  -webkit-appearance: none; /* Override default look */
  appearance: none;
  width: 14px; /* Set a specific slider handle width */
  height: 14px; /* Slider handle height */
  background: #eee; /* Green background */
  cursor: pointer; /* Cursor on hover */
  border: 1px solid gray;
  border-radius: 100%;
}

.min-max-slider > input::-webkit-slider-runnable-track {cursor: pointer;}
<div class="min-max-slider" data-legendnum="2">
    <label for="min">Minimum price</label>
    <input id="min" class="min" name="min" type="range" step="1" min="0" max="3000" />
    <label for="max">Maximum price</label>
    <input id="max" class="max" name="max" type="range" step="1" min="0" max="3000" />
</div>

I've tried to create the same sort of component using React. But each thumbgrip can only slide to the mid-point, where as with the non-react example, the lesser slider simply can't be higher than the higher slider and vice versa.

I would also like some way to make the slider which outside the range of the thumbgrips a darker color.

var thumbsize = 14;

const Slider = ({min, max}) => {
    const avg = (min + max)/2;
  const width = 300
  const minWidth = thumbsize + ((avg - min)/(max - min))*(width - (2*thumbsize))
  const styles={
      min:{
        width: minWidth,
        left: 0
      },
      max:{
        width: thumbsize + ((max - avg)/(max - min))*(width - (2*thumbsize)),
        left: minWidth
      }
  }
  
    return (
    <div 
      class="min-max-slider"
      data-legendnum="2"
      data-rangemin={min}
      data-rangemax={max}
      data-thumbsize={thumbsize}
      data-rangewidth={width}
    >
      <label htmlFor="min">Minimum price</label>
      <input 
        id="min"
        className="min"
        style={styles.min}
        name="min"
        type="range"
        step="1"
        min={min}
        max={avg}
        data-value={min}
       />
      <label htmlFor="max">Maximum price</label>
      <input
        id="max"
        className="max"
        style={styles.max}
        name="max"
        type="range"
        step="1"
        min={avg}
        max={max}
        data-value={max}
       />
    </div>
  )
}

ReactDOM.render(<Slider min={300} max={3000} />, document.querySelector("#root"))
 



.min-max-slider {
  position: relative;
  width: 200px;
  text-align: center;
  margin-bottom: 50px;
 }

.min-max-slider > label {
  display: none;
 }
 
.min-max-slider > input {
  cursor: pointer;
  position: absolute;
}


/* webkit specific styling */
.min-max-slider > input {
  -webkit-appearance: none;
  outline: none!important;
  background: transparent;
  background-image: linear-gradient(to bottom, transparent 0%, transparent 30%, silver 30%, silver 60%, transparent 60%, transparent 100%);
}

.min-max-slider > input::-webkit-slider-thumb {
  -webkit-appearance: none; /* Override default look */
  appearance: none;
  width: 14px; /* Set a specific slider handle width */
  height: 14px; /* Slider handle height */
  background: #eee; /* Green background */
  cursor: pointer; /* Cursor on hover */
  border: 1px solid gray;
  border-radius: 100%;
}

.min-max-slider > input::-webkit-slider-runnable-track {cursor: pointer;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

UPDATE

Using lissetdm's solution below, I have been able to create a double sided slider, which you can view on this sandbox, but there are still a few problems with it.

  1. The orange doesn't always look like it overlaps the gray

enter image description here

Sometimes, unpredictably, this difference is very noticeable

enter image description here enter image description here

  1. When both thumb pieces are dragged to the edges, the one pushes the other down

enter image description here

  1. I can't seem to remove this outline, which doesn't always appear

enter image description here


I have tested this out in firefox

like image 370
Sam Avatar asked Dec 01 '20 17:12

Sam


Video Answer


2 Answers

As react works, you need to move avg to the component's state and during input onChange event update it's value:

const Slider = ({ min, max }) => {
  const [avg, setAvg] = useState((min + max) / 2);
  const [minVal, setMinVal] = useState(avg);
  const [maxVal, setMaxVal] = useState(avg);

  const width = 300;
  const minWidth =
    thumbsize + ((avg - min) / (max - min)) * (width - 2 * thumbsize);
  const styles = {
    min: {
      width: minWidth,
      left: 0
    },
    max: {
      width: thumbsize + ((max - avg) / (max - min)) * (width - 2 * thumbsize),
      left: minWidth
    }
  };

  useEffect(() => {
    setAvg((maxVal + minVal) / 2);
  }, [minVal, maxVal]);


  return (
    <div
      className="min-max-slider"
      data-legendnum="2"
      data-rangemin={min}
      data-rangemax={max}
      data-thumbsize={thumbsize}
      data-rangewidth={width}
    >
      <label htmlFor="min">Minimum price</label>
      <input
        id="min"
        className="min"
        style={styles.min}
        name="min"
        type="range"
        step="1"
        min={min}
        max={avg}
        value={minVal}
        onChange={({ target }) => setMinVal(Number(target.value))}
      />
      <label htmlFor="max">Maximum price</label>
      <input
        id="max"
        className="max"
        style={styles.max}
        name="max"
        type="range"
        step="1"
        min={avg}
        max={max}
        value={maxVal}
        onChange={({ target }) => setMaxVal(Number(target.value))}
      />
    </div>
  );
};

UPDATED

I can't seem to remove this outline, which doesn't always appear

This line appears because the browser detects that the input is invalid. To disable this effect use:

input[type="range"]:invalid {
  box-shadow: none;
}

The orange doesn't always look like it overlaps the gray

And

When both thumb pieces are dragged to the edges, the one pushes the other down

I can't reproduce this scenarios but I recommend you to control the background color using the min and max position:

CSS:

input[type="range"] {
  --minRangePercent: 0%;
  --maxRangePercent: 0%;
  height: .4rem;
}

.min-max-slider > input.min {
  background-image: linear-gradient(
    to right,
    silver 0%,
    silver var(--minRangePercent),
    orange var(--minRangePercent),
    orange 100%
  );
}

.min-max-slider > input.max {
  background-image: linear-gradient(
    to right,
    orange 0%,
    orange var(--maxRangePercent),
    silver var(--maxRangePercent),
    silver 100%
  );
}

JS:

const minPercent = ((minVal - min) / (avg - min)) * 100;
const maxPercent = ((maxVal - avg) / (max - avg)) * 100;
const styles = {
    min: {
      width: minWidth,
      left: 0,
      "--minRangePercent": `${minPercent}%`
    },
    max: {
      width: thumbsize + ((max - avg) / (max - min)) * (width - 2 * thumbsize),
      left: minWidth,
      "--maxRangePercent": `${maxPercent}%`
    }
 };

Working example

like image 114
lissettdm Avatar answered Sep 25 '22 07:09

lissettdm


The problem was that min and max are strings so calculating the avg was incorrect but by using parseInt you will get integers as expected. here i created a sandbox but i had to change the value of width from 30 to 300 to make enough space for the dots.

like image 32
Alan Omar Avatar answered Sep 22 '22 07:09

Alan Omar