[Vue.js]ドロップダウン(b-form-select)の表示内容を動的にフィルタリングしつつ、同時にテーブル(b-table)のアイテムをフィルタリングする方法

以前投稿した、「BootstrapVueのテーブル(b-table)で複数条件によるフィルタを実装する方法」では、複数の条件に従ってテーブルアイテムの内容がフィルタリングされるようにしましたが、今回は、条件として複数のドロップダウン(b-form-select)を使うだけでなく、1つ目のドロップダウンの内容に応じて2つ目のドロップダウンの内容を動的にフィルタリングしながら、同時にテーブルアイテムをフィルタリングする方法を説明します。

使うデータと基本のテーブル

今回は、2019年から2022年までに開催された、もしくはされる予定のサッカーの国際大会決勝をドロップダウンで年、月を絞り込みながら表示アイテムを絞り込んでいく、というシナリオを考えます。
使用するデータは以下の通りです。

<template>
  <div>
    <b-table
      class="text-left"
      :items="items"
    >>
    </b-table>
  </div>
</template>
<script>
export default {
  data() {
    return {
      items: null,
    }
  },
  mounted() {
    this.items = DATA
  },
}

const DATA = [
  { year: 2019, month: 2, sports_event: '2019 AFC Asian Cup Final' },
  { year: 2019, month: 7, sports_event: '2019 FIFA Women\'s World Cup Final' },
  { year: 2021, month: 1, sports_event: '2020 African Nations Championship Final' },
  { year: 2021, month: 2, sports_event: '2020 FIFA Club World Cup Final' },
  { year: 2021, month: 7, sports_event: 'UEFA Euro 2020 Final' },
  { year: 2022, month: 12, sports_event: '2022 FIFA World Cup Final' },
]
</script>

ドロップダウン(b-form-select)の設置

まずは、絞り込み用のドロップダウンを設置していきます。今回は、年、月の2つのドロップダウンを設置していきます。

<template>
  <div>
    <b-form-select
      v-model="year"
      :options="year_list"
      class="mb-3"
    ></b-form-select>
    <b-form-select
      v-model="month"
      :options="month_list"
      class="mb-3"
    ></b-form-select>
    <b-table
      class="text-left"
      :items="items"
    ></b-table>
  </div>
</template>

データプロパティには、ドロップダウンで選択した値を保持するyearと、ドロップダウンに表示するリストを保持するyear_listを用意します。月も同様です。

export default {
  data() {
    return {
      items: null,
      year: "",
      year_list: [],
      month: "",
      month_list: [],
    }
  },
  mounted() {
    this.items = DATA
  },
}

ドロップダウンに表示するリストを作成します。

mounted() {
    this.items = DATA
    DATA.map(item => this.year_list.push(item.year))
    DATA.map(item => this.month_list.push(item.month))
  },

年、月共にリストに値が入りましたが、重複データもありますし、ソートもされていませんので、重複を排除し、ソートすることにします。

重複排除、ソート

重複の排除には以下のようなアロー関数を使うことにします。

mounted() {
    this.items = DATA
    DATA.map(item => this.year_list.push(item.year))
    DATA.map(item => this.month_list.push(item.month))

    this.year_list = this.year_list.filter((item, index) => this.year_list.indexOf(item) == index)
    this.month_list = this.month_list.filter((item, index) => this.month_list.indexOf(item) == index)
  },

重複が排除され、ドロップダウンがすっきりしました。次に、ソートをすることにします。

mounted() {
    this.items = DATA
    DATA.map(item => this.year_list.push(item.year))
    DATA.map(item => this.month_list.push(item.month))

    this.year_list = this.year_list.filter((item, index) => this.year_list.indexOf(item) == index)
    this.month_list = this.month_list.filter((item, index) => this.month_list.indexOf(item) == index)

    this.year_list = this.year_list.sort((a, b) => a - b)
    this.month_list = this.month_list.sort((a, b) => a - b)
  },

これで、検索用のドロップダウンの設置が完了しました。この状態では、ドロップダウンを選択しても特に何もおこりません。次に、本題である、ドロップダウンの内容を動的にフィルタリングするようにしてみます。

年(Year)の内容で、月(Month)のリスト項目を絞り込み

ここでは、例えばYearドロップダウンで2021を選択した際に、Monthドロップダウンとして、1, 2, 7のみが表示されるように、Year, Monthの順でNarrow Downするようにしてみます。今回はwatchプロパティを使う方法で実装してみます。

watch: {
    year: function (newVal) {
      this.month_list = []
      DATA.map(item =>
        (item.year == newVal && this.month_list.push(item.month))
      )
    },
  },

ドロップダウンの値が変更されると、このfunctionが呼び出され、選択した値がnewValとして連携されますので、まずはmonthドロップダウンのリストに表示するプロパティのmonth_listを初期化し、データの中から選択されたyearのものだけをmonth_listに格納し、monthドロップダウンに表示するようにしています。このコードを追加すると、以下のような振る舞いになります。

これで、1つ目のドロップダウンの値に2つ目のドロップダウンの値が連動し、動的にドロップダウンの表示項目が変化するようになりました。最後に、この2つのドロップダウンの値に、テーブルアイテムを連動させてみます。

テーブル(b-table)をドロップダウン(b-form-select)の内容でフィルタリング

以下の投稿で説明した方法を、ほぼそのまま流用することで、ドロップダウンの値とテーブルを連動させてみます。細かい部分は、以下の投稿を参考にしてみてください。

<b-table
      class="text-left"
      :items="items"
      :filter="filter"
      :filter-function="tableFilter"
></b-table>
methods: {
    tableFilter(row) {
      var filter_year = this.year ? String(row.year) == String(this.year) : true
      var filter_month = this.month ? String(row.month) == String(this.month) : true

      return filter_year && filter_month
    }
  },
computed: {
    filter: function () {
      return [this.year, this.month]
    }
  },

上記を実装した結果は以下の通りです。テーブルアイテムが絞り込めて表示できていることが分かると思います。

まとめ

2022年を選択した際に一度テーブルが消えてしまう、一度値を選択すると全検索(当該項目でのフィルタリングなし)ができない、など実用上はもう少しブラッシュアップが必要かと思いますが、冗長になってしまうので、今回はここまでとします。

最終型のソースコード

<template>
  <div>
    <b-form-select
      v-model="year"
      :options="year_list"
      class="mb-3"
    ></b-form-select>
    <b-form-select
      v-model="month"
      :options="month_list"
      class="mb-3"
    ></b-form-select>
    <b-table
      class="text-left"
      :items="items"
      :filter="filter"
      :filter-function="tableFilter"
    ></b-table>
  </div>
</template>
<script>
export default {
  data() {
    return {
      items: null,
      criteria: "",
      year: "",
      year_list: [],
      month: "",
      month_list: [],
    }
  },
  methods: {
    tableFilter(row) {
      var filter_year = this.year ? String(row.year) == String(this.year) : true
      var filter_month = this.month ? String(row.month) == String(this.month) : true

      return filter_year && filter_month
    }
  },
  computed: {
    filter: function () {
      return [this.year, this.month]
    }
  },
  watch: {
    year: function (newVal) {
      this.month_list = []
      DATA.map(item =>
        (item.year == newVal && this.month_list.push(item.month))
      )
    },
  },
  mounted() {
    this.items = DATA
    DATA.map(item => this.year_list.push(item.year))
    DATA.map(item => this.month_list.push(item.month))

    this.year_list = this.year_list.filter((item, index) => this.year_list.indexOf(item) == index)
    this.month_list = this.month_list.filter((item, index) => this.month_list.indexOf(item) == index)

    this.year_list = this.year_list.sort((a, b) => a - b)
    this.month_list = this.month_list.sort((a, b) => a - b)
  },
}

const DATA = [
  // https://en.wikipedia.org/wiki/2021_in_association_football
  { year: 2019, month: 2, sports_event: '2019 AFC Asian Cup Final' },
  { year: 2019, month: 7, sports_event: '2019 FIFA Women\'s World Cup Final' },
  { year: 2021, month: 1, sports_event: '2020 African Nations Championship Final' },
  { year: 2021, month: 2, sports_event: '2020 FIFA Club World Cup Final' },
  { year: 2021, month: 7, sports_event: 'UEFA Euro 2020 Final' },
  { year: 2022, month: 12, sports_event: '2022 FIFA World Cup Final' },
]
</script>

余談ですが、wikipediaを参考に今回のサンプルデータを作成しましたが、2020年のイベントが軒並み2021年に延期されていることを再認識せざるを得ませんでした…