A combination of Range Slider and Range Input

Hi there,

In my current project I have a requirement where I need to build a combination of Range Slider and Range Input (see screenshot).
range-input-slider

Is this possible to create with the Instantsearch.js library?

Thanks for any suggestions!

Hi aarongerig,

You can assign both the rangeSlider and rangeInput widgets to the same attribute. I built a quick demo to display, see the screenshot below:

Updating one of the widgets will dynamically update the other.

Is this what you were looking for?

-Kevin

1 Like

Hi Kevin,

Sometimes the solution is far simpler than you might think. I’ll gladly try to work it out with your suggestions and will let you know if it is what I was looking for.

Thanks a lot!

1 Like

Hi @kevin.sullivan,

Your suggestion helped me to achieve what I was looking for, thanks a lot!
Here’s the code of my custom rangeInputSlider built with noUiSlider and UIkit, if someone’s interested:

import noUiSlider from 'nouislider';
import 'nouislider/distribute/nouislider.css';
import { connectRange } from 'instantsearch.js/es/connectors';
import {
  $,
  append,
  attr,
  fragment,
  noop,
  on,
  toEventTargets,
  toNumber,
} from 'uikit/src/js/util';

const render = (renderOptions, isFirstRender) => {
  const {
    start,
    range,
    refine,
    widgetParams: { container, step },
  } = renderOptions;
  const [min, max] = start;

  if (isFirstRender) {
    const currentValue = 'data-current-value';
    const slider = fragment('<div></div>');
    const minInput = fragment('<input class="uk-input uk-form-small" name="min" type="number">');
    const maxInput = fragment('<input class="uk-input uk-form-small" name="max" type="number">');
    const minContainer = fragment('<div class="min-container uk-position-relative"></div>');
    const maxContainer = fragment('<div class="max-container uk-position-relative"></div>');
    const form = fragment(`
      <form class="uk-grid uk-grid-small uk-flex-middle uk-margin-small">
        <div class="uk-width-expand"></div>
        <div class="uk-width-auto"><span>–</span></div>
        <div class="uk-width-expand"></div>
      </form>
    `);

    // Create slider instance
    noUiSlider.create(slider, {
      range: { min: 0, max: 100 },
      step,
      start,
      connect: true,
      format: {
        to: (value) => value,
        from: (value) => toNumber(value),
      },
    });

    // Refine hits on slider change
    slider.noUiSlider.on('change', ([minValue, maxValue]) => {
      refine([minValue, maxValue]);
    });

    // Update value of input fields
    slider.noUiSlider.on('slide', ([minValue, maxValue]) => {
      minInput.value = Math.round((minValue + Number.EPSILON) * 100) / 100;
      maxInput.value = Math.round((maxValue + Number.EPSILON) * 100) / 100;
    });

    // Handle input of number fields
    on(toEventTargets([minInput, maxInput]), 'focus blur', ({ target, type }) => {
      // Bind current value to target element
      if (type === 'focus') {
        attr(target, currentValue, target.value);

        return;
      }

      // Skip further processing, if value has not changed
      if (target.value === attr(target, currentValue)) {
        return;
      }

      const isMin = attr(target, 'name') === 'min';
      const threshold = isMin ? attr(minInput, 'min') : attr(maxInput, 'max');

      let maxValue = !isMin ? target.value : maxInput.value || attr(maxInput, 'max');
      let minValue = isMin ? target.value : minInput.value || attr(minInput, 'min');

      // Rectify invalid min/max values
      if (isMin) {
        if (minValue < threshold) {
          minInput.value = threshold;
          minValue = threshold;
        } else if (minValue > maxValue) {
          minInput.value = maxValue;
          minValue = maxValue;
        }
      } else if (maxValue > threshold) {
        maxInput.value = threshold;
        maxValue = threshold;
      } else if (maxValue < minValue) {
        maxInput.value = minValue;
        maxValue = minValue;
      }

      refine([minValue, maxValue]);
    });

    // Add unit to input fields
    if (this.unit) {
      append(minContainer, fragment(`<span class="uk-form-icon uk-form-icon-flip">${this.unit}</span>`));
      append(maxContainer, fragment(`<span class="uk-form-icon uk-form-icon-flip">${this.unit}</span>`));
    }

    append(container, slider);
    append(minContainer, minInput);
    append(maxContainer, maxInput);
    append(form.firstElementChild, minContainer);
    append(form.lastElementChild, maxContainer);
    append(container, form);
  }

  const slider = $('.noUi-target', container);
  const minInput = $('input[name="min"]', container);
  const maxInput = $('input[name="max"]', container);

  // Set min/max range
  if (range.min !== range.max) {
    slider.noUiSlider.updateOptions({ range });
    attr(minInput, 'min', range.min);
    attr(maxInput, 'max', range.max);
  }

  // Set slider values
  if (Number.isFinite(min) && Number.isFinite(max)) {
    slider.noUiSlider.set([min, max]);
  } else {
    slider.noUiSlider.reset();
  }

  minInput.value = Number.isFinite(min) ? min : '';
  maxInput.value = Number.isFinite(max) ? max : '';
};

return connectRange(render, noop);

Cheers! :beers:

1 Like