見出し画像

別アプリ参照で3階層動的絞り込みドロップダウン

やりたいこと

kintoneで別アプリのレコードを参照したドロップダウンリストで、大分類と小分類を選択して、分類項目に一致するアイテムだけを絞り込み選択できる3階層動的絞り込みドロップダウンリストを実装したい。
この様な要望を頂きましたので、kintone UI Component v1Dropdownを利用して作成してみました。

デモ画面1

デモ画面では、別アプリ(商品メニュー)のレコード値の大分類、小分類、商品名の値を外部参照してをドロップダウンリストの値にしています。
大分類で小分類の選択範囲が変化し、大分類+小分類の組み合わせで商品名の選択範囲を絞り込みしてドロップダウンリストにしています。

3階層絞り込みドロップダウンデモ画面

下記の商品メニューアプリを参照して、ドロップダウンに表示しています。
大分類と小分類と商品名を参照しています。

商品メニューアプリの一覧表

アプリの設定

本稿の「動的絞り込みドロップダウン」は、他アプリのレコードをドロップダウンのリスト値として読み込みますので、商品検索アプリと商品マスタアプリの2つを準備します。

(1)商品マスタアプリの設定画面

ドロップダウンで参照するアプリには、以下のフィールドが必要です。
※商品IDは参照しませんのでなくても可ですが、マスターのキー項目になるので、設計段階で準備して置いた方が良いです。
既に作成しているマスタ関連のアプリを読み込みたい場合は、フィールド名とフィールドコードを既存アプリの設定内容に読み替えて下さい。

商品マスタのフォーム設定画面

(2)商品検索アプリの設定画面
 商品マスタを読み込んで動的絞り込みドロップダウンを設置する商品検索アプリの設定画面には、KUC版ドロップダウンを設置するスペース要素のみ準備します。

商品検索アプリの設定画面

動的絞り込みドロップダウンのJavascriptコード

動的絞り込みドロップダウンリストのサンプルコードです。
初期設定の部分で、ドロップダウンのリスト値として読み込みたいアプリの
アプリIDとフィールドコードを設定します。
サンプルコードは、新規レコード登録画面で動作する様にしています。
※動作確認のサンプルコードなのでフィールド値の保存機能は未実装です。

/* 絞り込みドロップダウン サンプルコード */
(function() {
  'use strict';

  // 初期設定
  const APP_ID = '123'; // 外部参照するアプリID
  const MAJOR_CATEGORY_FIELD = '大分類'; // 大分類フィールド
  const MINOR_CATEGORY_FIELD = '小分類'; // 小分類フィールド
  const PRODUCT_NAME_FIELD   = '商品名'; // 商品名フィールド
  const SPACE_FIELD = 'SPACE_FIELD';     // 表示スペース
  // ---- <初期設定 ここまで>----
  let allRecords = [];

  // ドロップダウンリストの作成
  const createDropdown = (label, optionsId) => {
    const dropdown = new Kuc.Dropdown({
      label: label,
      id: optionsId,
      items: [{ label: '----', value: '' }],
      value: ''
    });
    dropdown.style.marginLeft = '16px';
    return dropdown;
  };

  // 重複を排除してリストを作成
  const createUniqueList = (records, fieldCode) => {
    const uniqueValues = [...new Set(records.map(record => record[fieldCode].value))];
    return uniqueValues.map(value => ({
      label: value,
      value: value
    }));
  };

  // レコードの検索とすべての選択肢の初期化
  const initializeAllOptions = async () => {
    try {
      const resp = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
        app: APP_ID,
        query: ''
      });
      allRecords = resp.records;
    } catch (err) {
      console.error('Error fetching records:', err);
      alert('レコードの取得に失敗しました。');
    }
  };

  // ドロップダウンリストの初期化
  const clearDropdown = (dropdown) => {
    dropdown.items = [{ label: '----', value: '' }];
    dropdown.value = '';
  };

  // 小分類の更新
  const updateMinorCategories = (majorCategory) => {
    const filteredRecords = allRecords.filter(record => record[MAJOR_CATEGORY_FIELD].value === majorCategory);
    return createUniqueList(filteredRecords, MINOR_CATEGORY_FIELD);
  };

  // 商品名の更新
  const updateProductNames = (minorCategory) => {
    const filteredRecords = allRecords.filter(record => record[MINOR_CATEGORY_FIELD].value === minorCategory);
    return filteredRecords.map(record => ({
      label: record[PRODUCT_NAME_FIELD].value,
      value: record[PRODUCT_NAME_FIELD].value
    })).sort((a, b) => a.label.localeCompare(b.label));
  };

  // ドロップダウンリストの作成とイベントの設定
  const setupDropdowns = async (record) => {
    const majorCategoryDropdown = createDropdown('大分類', 'id-majorCategory');
    const minorCategoryDropdown = createDropdown('小分類', 'id-minorCategory');
    const productNameDropdown   = createDropdown('商品名', 'id-productName');

    await initializeAllOptions();

    const majorCategories = createUniqueList(allRecords, MAJOR_CATEGORY_FIELD);
    majorCategoryDropdown.items = [{ label: '----', value: '' }, ...majorCategories];

    majorCategoryDropdown.addEventListener('change', () => {
      const majorValue = majorCategoryDropdown.value;
      if (majorValue) {
        minorCategoryDropdown.items = [{ label: '----', value: '' }, ...updateMinorCategories(majorValue)];
        clearDropdown(productNameDropdown);
      } else {
        clearDropdown(minorCategoryDropdown);
        clearDropdown(productNameDropdown);
      }
      kintone.app.record.set({ record: record });
    });

    minorCategoryDropdown.addEventListener('change', () => {
      const minorValue = minorCategoryDropdown.value;
      if (minorValue) {
        productNameDropdown.items = [{ label: '----', value: '' }, ...updateProductNames(minorValue)];
      } else {
        clearDropdown(productNameDropdown);
      }
      kintone.app.record.set({ record: record });
    });

    // ドロップダウンリストを画面に追加
    const spaceElement = kintone.app.record.getSpaceElement(SPACE_FIELD);
    spaceElement.appendChild(majorCategoryDropdown);
    spaceElement.appendChild(minorCategoryDropdown);
    spaceElement.appendChild(productNameDropdown);
  };

  // レコード新規登録画面のイベントで動作
  kintone.events.on(['app.record.create.show'], async (event) => {
    await setupDropdowns(event.record);
    return event;
  });

})();

絞り込みドロップダウンリストの仕様として、下記機能を実装しています。
①大分類の変更イベントで、小分類と商品名のドロップダウンを初期化
②小分類の変更イベントで、商品名のドロップダウンを初期化
また、他アプリのレコードを読み込む処理に時間がかかる場合があるので、レコード検索処理とドロップダウン表示処理が同期する様にasync/awaitの処理を行っています。
参照:async/awaitとは
これら処理を実装したため、少し長いコードになっています。


アプリの設定>JavaScript / CSSでカスタマイズ

KUC版テーブルを設置するアプリの、アプリの設定>JavaScript / CSSでカスタマイズの画面で、kintone UI ComponentのCDNをURL指定で追加し、次に初期設定済のJavascriptコードをアップロードして追加します。

最新版の kintone UI Component は、以下のCDNで利用できます。https://unpkg.com/kintone-ui-component/umd/kuc.min.js

https://kintone-ui-component.netlify.app/ja/docs/getting-started/quick-start/

【注意事項】

kintone UI ComponentのCDN版は動作試験の目的でのみご利用ください。
本番環境で利用する際はUMD版が推奨です。
詳しくは、以下の記事を参照して下さい。


スタイルのCSSカスタムコード

デモ画面の様に、商品名のドロップダウンフィールドの横幅と文字色を変更する「KUC版ドロップダウン」専用のCSSコードの記載例を掲載しておきます。
Javascriptコードのプロパティ指定 ' id: id-productName'に合わせて利用して下さい。

#id-productName {
    --kuc-dropdown-toggle-width: 260px;
    --kuc-dropdown-menu-color: green;
    --kuc-dropdown-menu-color-selected: red;
    --kuc-dropdown-toggle-color: blue;
  }

デモ画面2(本番仕様)

デモ画面2では、大分類と小分類で絞り込み選択した商品名の商品コードと金額を参照先のアプリのデータを取得して表示しています。

本番環境では、倉庫の部品を大分類と小分類で絞り込み表示された部品名の選択で、コード番号と棚番号を自動的に表示するという処理に用いました。
また、絞り込みドロップダウンで選択し表示した結果をレコードに保存する処理とレコード編集の処理も実装しました。

デモ画面2

カスタマイズした感想

分類で絞り込み表示できるドロップダウンは、結構ニーズが有る機能なので有料のプラグインも色々と紹介されていますが、今回は、デモ画面2の様な独自仕様(絞り込み選択した商品のコードと金額を表示)の実装を求められたので、カスタマイズに挑戦してみました。

最初はkintone UI Component v1のdropdownコンポーネントを使えば簡単にできると思っていましたが、思いのほか苦労しました。
苦労した点は、別アプリのレコード検索結果とドロップダウンリストへ表示する処理が同期する様にしたこと、レコード絞り込み検索とドロップダウンの表示の同期処理と大分類や小分類を変更するイベント発生で下位のドロップダウンのリスト値を初期化する処理を共通関数化して実装したことです。

他アプリのレコードを読み込む単一のドロップダウンの処理内容と比較すると、分類項目に関連性を持たせた絞り込み型のドロップダウンの場合では、ドロップダウンのリスト値の変更イベントで行うべき処理(検索の再実行、下位リストの初期化など)が格段に増えることが良く分かりました。

今回も、最後まで読んで頂いてありがとうございました。

よろしければサポートお願いします! いただいたサポートは、note記事制作の活動費に使わせていただきます!