😈

2つの摘みを持ったレンジバーを作ってみた

に公開

個人開発のアプリの一環にゾゾタ⚪︎ンのような価格の絞り込み機能を作ってみたので共有します
参考にさせて頂いた動画

https://youtu.be/zksYICkbBbY?si=uVZ3QMiP9ApUrDVT

実際の使用感は以下のような感じ

bladeとコントローラー

home.blade.php
<form action="{{route('app.home.search')}}" method="get" class="flex items-center justify-start" id="searchForm">
    <div>
        <h2 class="rangeText">価格帯を選択</h2>
        <div class="container">
            {{-- low値を表示するコンテナ --}}
            <div class="low-num-container">
                <input type="number" id="low-num" value="0" disabled>
            </div>
            <div class="slider-container">
                {{--価格帯のバー --}}
                <div class="range-bar" id="range-bar"></div>
                {{-- 二つのrangeを用意 --}}
                <input type="range" name="highPrice" id="high" class="high" min="0" max="300000" step="5000" value="{{request('highPrice') ?? 300000}}">
                <input type="range" name="lowPrice" id="low" class="low" min="0" max="300000" step="5000" value="{{request('lowPrice') ?? 0}}">
            </div>
            {{-- high値を表示するコンテナ --}}
            <div class="high-num-container">
                <input type="number"id="high-num" value="300000" disabled>
            </div>
        </div>
    </div>
</form>

方針としてはrangeの丸みを二つとrangeバーを重ね合わせて一つのrangeバーのようにします
input:numberにrangeの値を入れることでどんな値かを表示してみます
value="{{request('highPrice') ?? 300000}}で入力された項目を保持します
コントローラーは以下の通り(本題とずれているので軽く紹介)

HomeController.php
public function search(Request $request) : View
    {
        $lowPrice = $request->input('lowPrice', 0);
        $highPrice = $request->input('highPrice', 300000);
        $query = Part::query()->with(['reviews','category']);
        // 価格帯検索
        $query->whereBetween('price',[$lowPrice,$highPrice]);
        // 取得
        $parts = $query->get();
        return view('app/home',['parts' => $parts,'categories' => $categories]);
    }

css

priceBar.css
.container{
    display: flex;
    position: relative;
    width: 600px;
    height: 50px;
    align-items: center;
    justify-content: center;
    border-radius: 20px;
    background: #d1d5db;
}

.rangeText{
    margin: 0 0 6px 10px;
}

.container input[type="number"]{
    width: 80px;
    height: 30px;
    background: #fff;
    border: 1px #eee solid;
    font-size: 15px;
    font-weight: 700;
    text-align: center;
    border-radius: 8px;
}
/* ブラウザにデフォルトでついてるnumberのスピンボタンを削除 */
.container input[type="number"]::-webkit-outer-spin-button,
.container input[type="number"]::-webkit-inner-spin-button{
    -webkit-appearance: none;
}

.slider-container{
    position: relative;
    width: 400px;
    height: 6px;
    background: #eee;
    outline: none;
    margin: 10px;
}

.range-bar{
    height: 100%;
    border-radius: 50px;
    background: #3e3e3e;
    top: 50%;
    transform: translateY(-50%);
    position: absolute;
    width: 100%;
}

.slider-container input[type="range"]{
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    -webkit-appearance: none;
    outline: none;
    background: none;
    width: 100%;
}

.slider-container input[type="range"]::-webkit-slider-thumb{
    pointer-events: all;
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    background: black;
    border-radius: 50px;
    border: 2px #fff solid
}

親のdivにrelativeとグレーの背景をつけておき、そこに選択された範囲の示す黒色のバーを重ね合わしています
webkit-appearance:noneをつけることでブラウザについてくるデフォルトの仕様をなくせます

js

jsでは以下の二つのことを考えます

  • lowのrangeとhighのrangeが逆転しないようにする
  • lowとhighの値を取得して.range-barの位置を更新する
priceBar.js
document.addEventListener('DOMContentLoaded',() => {
    const lowSlider = document.getElementById('low');
    const highSlider = document.getElementById('high');
    const lowNum = document.getElementById('low-num');
    const highNum = document.getElementById('high-num');
    const rangeBar = document.getElementById('range-bar');
    const form = document.getElementById('searchForm');

    const minPrice = 0;
    const maxPrice = 300000;
    const step = 5000;

    let timer;

    function updateSlider(event) {
        // intに変換
        let lowVal = parseInt(lowSlider.value);
        let highVal = parseInt(highSlider.value);
        // low値とhigh値に下限、上限を持たせる low値は0~295000まで high値は5000~300000まで
        lowVal = Math.max(minPrice,Math.min(lowVal,maxPrice - step));
        highVal = Math.min(maxPrice,Math.max(highVal,minPrice + step));
        // highとlowの差が逆転するとき
        if (highVal - lowVal < step) {
            // low > highになろうとしているとき
            if (event.target === lowSlider) {
                lowVal = highVal - step;
            }
            // high < lowになろうとしているとき
            else {
                highVal = lowVal + step;
            }
        }

        // numberとrangeの更新
        lowSlider.value = lowVal;
        highSlider.value = highVal;
        lowNum.value = lowVal;
        highNum.value = highVal;

        // range-barのmaxとminの位置を取得
        const max = ( highVal / maxPrice ) * 100;
        const min = ( lowVal / maxPrice ) * 100;
        // range-barを更新
        rangeBar.style.left = min + '%';
        rangeBar.style.width = ( max - min ) + '%';
    }
    // レンジバー変更後に自動送信する関数 400ミリ秒立ったら自動で更新
    function submit(){
        clearTimeout(timer);
        timer = setTimeout(() => {
            form.submit();
        },400)
    }

    // イベント設定
    [lowSlider,highSlider].forEach(slider => {
        slider.addEventListener('input',updateSlider);
        slider.addEventListener('input',submit);
    })

    // 初期設定
    updateSlider();
})

lowVal = Math.max(minPrice,Math.min(lowVal,maxPrice - step));のところはなくても動くと思いますが一応明示しておきました。low値は最小値(0)を下回ることなく、最大値はの上限から -1ステップ分の(295000)まで high値も同様です

終わり

これで終わりです。多分動くはず
rangeの値をstep=5000で固定するのではなく、[5000,10000,20000,40000,...]のようにできたらより使いやすいのかと思ったけどできなかった。。。 🥺

Discussion