20行でできる!JavaScriptで作る簡単なフィルタ機能

UI開発者 宇賀

製品一覧やセミナー一覧など、複数のアイテムが並んでいるページでは、たびたびフィルタ機能が求められることがあります。

ここで言うフィルタとは、ある程度の量がある情報の中からユーザーが求めている情報(アイテム)だけに絞り込む機能を指します。

フィルタ機能の実装方法

フィルタ機能を実装する方法にはいくつか手段があります。たとえば次のようなパターンです。

  • JSONやXMLに書かれた情報から、条件に応じて必要なアイテムだけを抽出して出力する
  • 条件をサーバーに送信し、データベースから取得されたアイテムを出力する
  • 予めすべてのアイテムをHTMLに記述しておき、条件に応じて不要なアイテムを削除、または非表示にする

いずれもメリットとデメリットが異なります。例に挙げたもの以外の方法も含め、要所要所で最適な方法で実装するべきでしょう。

今回は例の中で登場した3個目のパターンについて取り上げたいと思います。

実際のコード

フィルタ機能を実現するための簡易的なマークアップを用意します。実装する具体的な内容は「選択されたカテゴリに応じて、お寿司のネタを出しわける」というものです。

※ 次のサンプルではHTMLの中にCSS(style要素)が記述されていますが、通常は外部リソースにしてlink要素で読み込むべきです。

<style>
[data-filter-view]:not([data-filter-view=""]) [data-filter-key] {
    display: none;
}
[data-filter-view][data-filter-view~="貝類"] [data-filter-key="貝類"],
[data-filter-view][data-filter-view~="光物"] [data-filter-key="光物"],
[data-filter-view][data-filter-view~="白身"] [data-filter-key="白身"],
[data-filter-view][data-filter-view~="赤身"] [data-filter-key="赤身"] {
    display: block;
}
</style>

<div class="filter" id="js-filter">
<ul class="filter-cond">
<li><label><input type="checkbox" value="貝類">貝類</label></li>
<li><label><input type="checkbox" value="光物">光物</label></li>
<li><label><input type="checkbox" value="白身">白身</label></li>
<li><label><input type="checkbox" value="赤身">赤身</label></li>
<!-- /.filter-cond --></ul>

<ul class="filter-items">
<li data-filter-key="貝類">つぶ貝</li>
<li data-filter-key="白身">カンパチ</li>
<li data-filter-key="赤身">マグロ</li>
<li data-filter-key="光物">コハダ</li>
<li data-filter-key="貝類">バイ貝</li>
<li data-filter-key="白身">マダイ</li>
<li data-filter-key="貝類">サザエ</li>
<!-- /.filter-items --></ul>
<!-- /#js-filter--></div>

続いて、前述のHTMLとCSSでフィルタ機能を動かすためのJavaScriptコードを用意します。

今回のフィルタ機能でJavaScriptが行うのは「チェックボックスが操作されたタイミングで要素の属性を変更する」処理のみです。

var widget = document.getElementById('js-filter');
var checkboxes = widget.querySelectorAll('.filter-cond input[type="checkbox"]');
var checkedList = [];
var filter = function () {
    checkedList = [];

    Array.prototype.forEach.call(checkboxes, function (input) {
        if (input.checked) {
            checkedList.push(input.value);
        }
    });

    widget.setAttribute('data-filter-view', checkedList.join(' '));
};

Array.prototype.forEach.call(checkboxes, function (checkbox) {
    checkbox.addEventListener('change', filter);
});

あるいは、次のように書くこともできます。

var widget = document.getElementById('js-filter');
var checkboxes = widget.querySelectorAll('.filter-cond input[type="checkbox"]');
var checkedList = [];
var i = 0;
var leng = 0;
var filter = function () {
    checkedList = [];

    i = 0;
    leng = checkboxes.length;

    for (i; i < leng; i++) {
        if (checkboxes[i].checked) {
            checkedList.push(checkboxes[i].value);
        }
    }

    widget.setAttribute('data-filter-view', checkedList.join(' '));
};


i = 0;
leng = checkboxes.length;

for (i; i < leng; i++) {
    checkboxes[i].addEventListener('change', filter);
}

これら2つのJavaScriptコード断片は、いずれも実行結果が等価なものです。ただし、どちらもフィルタ機能がページ内に1つ限りであるという想定の下記述されています。ですからフィルタ機能が同一ページに複数個存在する場合は、フィルタ機能の数だけfor文やArray.prototype.forEach.Callなどを用いて処理を行う必要があります。

操作サンプル

次のサンプルは、先述のコードを実際に動かした例です。少しUIのスタイルに手を加えてあります。

フィルタ機能の実装例
  • つぶ貝
  • カンパチ
  • マグロ
  • コハダ
  • バイ貝
  • マダイ
  • サザエ

仕組み

仕組み自体は実にシンプルです。次のCSSを見てみましょう。

[data-filter-view]:not([data-filter-view=""]) [data-filter-key] {
    display: none;
}

最初のCSSルールセットでは「data-filter-view属性が空でなければ、子孫のdata-filter-key属性を持つ要素のdisplayプロパティはnoneになる」ということが書かれています。

[data-filter-view][data-filter-view~="貝類"] [data-filter-key="貝類"],
[data-filter-view][data-filter-view~="光物"] [data-filter-key="光物"],
[data-filter-view][data-filter-view~="白身"] [data-filter-key="白身"],
[data-filter-view][data-filter-view~="赤身"] [data-filter-key="赤身"] {
    display: block;
}

2つ目のルールセットでは、「data-filter-view属性の値は、いくつかの文字列がスペース区切りで列挙されており、そのうち1つが完全一致する値をdata-filter-key属性に持つ子孫要素はdisplayプロパティがblockになる」ということが書かれています。フィルタリングさせたいカテゴリの数だけ、このルールセットのセレクタを記述しています。

JavaScriptではdata-filter-view属性値を操作する処理を行います。

var filter = function () {
    checkedList = [];

    Array.prototype.forEach.call(checkboxes, function (input) {
        if (input.checked) {
            checkedList.push(input.value);
        }
    });

    widget.setAttribute('data-filter-view', checkedList.join(' '));
};

Array.prototype.forEach.call(checkboxes, function (checkbox) {
    checkbox.addEventListener('change', filter);
});
  1. チェックボックスの値が変更されるタイミングで、filter関数が呼び出されます
  2. 選択されているチェックボックスのvalue属性値を配列に格納します
  3. ルートの要素に、配列を半角スペースで.join()した値を持つdata-filter-view属性をセットします

このようにすることで、DOM構造を組み替えずにCSSによる描画処理の力で要素の出しわけが行えるため、比較的低コストにフィルタ機能が実現できます。

しかし、CSSやJSを無効にしてしまうとすべてのコンテンツが見える状態になってしまうため、隠された情報がユーザーに伝わってはならない場面では、このやり方はあまり向かない実装方法です。

出しわけを行うアイテム数が膨大であればあるほど、HTMLに記述されていくソースの量も膨大になっていくため、コンテンツ量や出しわけを実現したい理由なども含めて、最適な方法で実装していけると良いですね。