kintoneのサブテーブルにデータフィルター機能を追加する

前回の記事では、kintoneのサブテーブルでデータを並び替える(ソートする)方法について解説し、情報を希望する順序で表示できるようにしました。
参照; kintoneでテーブルの行をデータの昇順・降順で並べ替える
しかし、多くの場合、並び替えるだけでは十分ではありません。特定の条件を満たす行だけを表示するために、データを絞り込む(フィルターする)必要もあります。
例:「ボールペン」という名前の製品の行だけを表示する。

実際には、「等しい (equal)」、「より大きい (greater than)」、「より小さい (less than)」、「含む (contains)」など、多くの種類のフィルター条件が存在します。
この記事をより分かりやすくするために、ここでは「等しい」という条件に絞って解説します。基本となる実装方法を理解すれば、他の条件にも応用することが可能です。

準備アプリ
前回の記事で作成したアプリを引き続き使用します。
Excelのデータをkintoneのテーブルにまとめてコピー&ペーストしたい(やれそうでやれないことの補完)

JavaScriptプログラム
※「JSEdit for kintone」プラグインを使用して、ソースコードを編集します。JSEdit for kintone の画面から jQuery を追加・インストールすることで、HTML 要素のコーディングがより簡単になります。

プログラムの処理流れ

  1. サブテーブル「商品一覧」のHTML要素を特定する
  2. フィルター状態の初期化
    • フィルター状態を保存するための変数(FILTER_STATE)を作成し、各列で選択されたフィルター値を格納します。
    • 各列には、1つまたは複数のフィルター値が選択される可能性があります。
  3. フィルターアイコンを列のヘッダーに追加する
    • 各サブテーブルの列にフィルターアイコンを追加します。
    • このアイコンは、フィルター設定用のポップアップを表示するためのトリガーとして機能します。
  4. フィルター用ポップアップの表示
    • フィルターアイコンがクリックされると、列のヘッダー直下にポップアップが表示されます。
    • このポップアップには、以下の要素が含まれます。
      • 「すべて選択」チェックボックス
      • その列に含まれる一意な値ごとのチェックボックス
      • フィルターを適用するための「適用」ボタン
  5. フィルター値の選択
    • ユーザーは、個々の値を選択または選択解除したり、「すべて選択」チェックボックスを使って一括で操作したりすることができます。
    • これらのチェックボックスの状態は、FILTER_STATEと同期して更新されます。
  6. フィルターの適用
    • 「適用」ボタンがクリックされた後、以下の処理を実行します。
      • 選択された値に基づいてFILTER_STATEを更新します。
      • サブテーブルのすべての行をループし、フィルター条件を満たさない行を非表示にし、条件を満たす行を表示します。
  7. ポップアップの外をクリックした際の非表示処理
    • ポップアップの外側をクリックすると、データが隠れないようにポップアップが自動的に閉じます。

ソースコード

(function() { 
  "use strict";

  const FORM_DATA = cybozu.data.page['FORM_DATA'];

  // サブテーブルのフィールドIDを取得
  const ELEMENT_FIELD_ID = {};
  for (const subTableId of Object.keys(FORM_DATA.schema.subTable)) {
    const subTable = FORM_DATA.schema.subTable[subTableId];
    ELEMENT_FIELD_ID[subTable.var] = subTableId;
  }

  const tableFieldCode = '商品一覧'; // 対象のサブテーブル名
  let FILTER_STATE = {}; // 選択されたフィルターの状態を保持するオブジェクト

  /**
   * 全てのフィルターを適用し、行を表示/非表示する
   * @param {Object} rec - レコードデータ
   * @param {string} tableFieldCode - サブテーブルのフィールドコード
   * @param {string} subtableClass - サブテーブルのDOMクラス
   */
  function applyAllFilters(rec, tableFieldCode, subtableClass) {
    const rows = rec[tableFieldCode].value;
    for (let i = 0; i < rows.length; i++) {
      // 各フィルター条件に一致するかチェック
      const visible = Object.keys(FILTER_STATE).every(fieldCode => {
        const selected = FILTER_STATE[fieldCode];
        const val = rows[i].value[fieldCode].value || "";
        if (!selected || selected.length === 0) return true; // 選択なし = 全て表示
        return selected.includes(val);
      });
      $(`${subtableClass} tbody tr`).eq(i).css('display', visible ? '' : 'none');
    }
  }

  // レコード作成/編集画面表示イベント
  kintone.events.on(['app.record.create.show', 'app.record.edit.show'], function(event) {
    const rec = event.record;
    const subtableClass = `.subtable-${ELEMENT_FIELD_ID[tableFieldCode]}`;

    // サブテーブルの各列にフィルターアイコンを追加
    $(`${subtableClass} .subtable-label-gaia`).each(function(index) {
      const th = $(this);

      // filter iconをまだ作成していなければ追加
      if (th.find('.filter-icon').length === 0) {
        th.append(`<span class="filter-icon" style="display:inline-block;width:16px;height:16px;line-height:16px;text-align:center;font-size:12px;font-weight:bold;cursor:pointer;margin-left:5px;border:1px solid #000;border-radius:4px;background:#fff;color:#000;user-select:none;">▼</span>`);
      }

      // アイコンクリックでフィルターポップアップを表示
      th.find('.filter-icon').off('click').on('click', function(e) {
        e.stopPropagation();
        $('.filter-popup').remove(); // 既存ポップアップ削除

        const fieldCode = Object.keys(rec[tableFieldCode].value[0].value)[index];
        const values = rec[tableFieldCode].value.map(row => row.value[fieldCode].value || "");
        const uniqueValues = [...new Set(values)]; // 重複を排除してユニーク値リスト作成

        // ポップアップ作成(inline style)
        const popup = $(`<div class="filter-popup" style="position:absolute;background:#fff;border:1px solid #000;padding:8px;border-radius:5px;box-shadow:0 2px 6px rgba(0,0,0,0.2);z-index:1000;max-height:200px;overflow-y:auto;font-size:13px;"></div>`);

        // 「全て選択」チェックボックス
        const allChecked = !FILTER_STATE[fieldCode] || FILTER_STATE[fieldCode].length === uniqueValues.length;
        popup.append(`<div style="margin-bottom:5px;"><input type="checkbox" id="chk_all" ${allChecked ? "checked" : ""}><label for="chk_all" style="cursor:pointer;margin-left:5px"><b>(全て選択)</b></label></div>`);

        // 各ユニーク値に対するチェックボックス
        uniqueValues.forEach((val, i) => {
          const safeVal = val || '(空白)'; // 空白の場合の表示
          const id = 'chk_' + i;
          const isChecked = !FILTER_STATE[fieldCode] || FILTER_STATE[fieldCode].includes(val);
          popup.append(`<div style="margin-bottom:3px;"><input type="checkbox" id="${id}" value="${val}" ${isChecked ? 'checked' : ''}><label for="${id}" style="cursor:pointer;margin-left:5px">${safeVal}</label></div>`);
        });

        // 適用ボタン
        popup.append('<div style="margin-top:8px;text-align:right;"><button class="apply-filter" style="padding:3px 8px;cursor:pointer;">適用</button></div>');

        $('body').append(popup);
        const offset = th.offset();
        popup.css({ top: offset.top + th.outerHeight(), left: offset.left });

        // 「全て選択」チェックボックス変更時
        popup.find('#chk_all').on('change', function() {
          const checked = $(this).is(':checked');
          popup.find('input[type=checkbox]').not(this).prop('checked', checked);
        });

        // フィルター適用ボタン
        popup.find('.apply-filter').on('click', function() {
          const selected = popup.find('input[type=checkbox]:checked').map(function() {
            return this.value;
          }).get();
          FILTER_STATE[fieldCode] = selected.length === uniqueValues.length ? [] : selected;
          applyAllFilters(rec, tableFieldCode, subtableClass);
          popup.remove();
        });

        // ポップアップ外クリックで閉じる
        $(document).on('click.filterPopup', function(ev) {
          if (!popup.is(ev.target) && popup.has(ev.target).length === 0) {
            popup.remove();
            $(document).off('click.filterPopup');
          }
        });
      });
    });

    return event;
  });
})();

結果


記事を読んでいただきありがとうございます。
次回の記事では、kintoneでプラグインを作成する方法をご紹介します。ぜひお楽しみに!