Vue.jsJapanese

[Vue.js]BootstrapVueのテーブル(b-table)で複数条件によるフィルタを実装する方法

Vue.js

BootstrapVueのテーブルコンポーネントの<b-table>を使うと、簡単にきれいなテーブルが作成できます。この作成したテーブルを条件によって絞り込んで表示しようと思います。b-tableには、Built-inでフィルタ機能が実装されており、これも簡単にテーブルデータをフィルタリングすることができるのですが、単純にこのフィルタ機能を使うだけでは、列を指定してのフィルタや、複数条件を絡めたフィルタリングを実現することができません。今回は、2つの条件を絡めたフィルタリングでデータを絞り込む方法を説明します。

参考;公式Docs:BootsatrapVue Filtering

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

[ads]

公式サイトにあるものと同じサンプルデータを使います。name, first_name, last_nameの3つの列からなる4つのレコードです。

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

}

const DATA = [
  { age: 40, first_name: 'Dickerson', last_name: 'Macdonald' },
  { age: 21, first_name: 'Larsen', last_name: 'Shaw' },
  { age: 89, first_name: 'Geneva', last_name: 'Wilson' },
  { age: 38, first_name: 'Jami', last_name: 'Carney' }
]

</script>

起動時に、mounted()でデータをテーブル表示用のコンポーネントプロパティであるitemsに読み込んで表示するだけのコードです。

Built-inフィルタ(全列対象)

[ads]

次に、Built-inフィルタについて説明します。Built-inフィルタは、b-tableのプロパティに検索用の文字列を指定するだけで有効になります。ここでは、入力用のb-form-inputを使って動的にテーブルの内容をフィルタリングしていきます。

<template>
  <div>
    <b-form-input v-model="search_text"></b-form-input>
    <b-table
      :items="items"
      :filter="search_text"
    ></b-table>
  </div>
</template>
export default {
  data() {
    return {
      items: null,
      search_text: "",
    }
  },
  mounted() {
    this.items = DATA
  },
}
8を含むものだけ表示
lを含むものだけ表示
mを含むものだけ表示

数字”8″を入力すると、8を含む行のみが表示されます。今回のデータはAge列のみ数字が入っていますので、89, 38の2行が表示されます。
文字列”l”や”m”を入力すると、それぞれの文字が含まれる行のみが表示されます。今回のデータはAge列以外に文字列が入っていますので、実施的にはAge列以外の2列を対象としてフィルタリングされます。

Built-inフィルタ(特定列対象)

[ads]

上述の通り、ほんの2〜3行追記するだけでフィルタリングが実装できます。ただし、実際の使用場面を考えれば、絞り込みたい列を指定することも一般的かと思います。そこで、次に特定の列でフィルタリングをする方法を説明します。

特定の1列を対象にフィルタリング

それでは、first_name列のみを対象にフィルタリングを実装してみます。これは、:filter-included-fieldsプロパティを1つ追加することで実現できます。

参考:Built in filtering options

<template>
  <div>
    <b-form-input v-model="search_text"></b-form-input>
    <b-table
      :items="items"
      :filter="search_text"
      :filter-included-fields="['first_name']"
    ></b-table>
  </div>
</template>

先程と同じ条件を入力した結果は以下のとおりです。数字を入れると、結果がゼロ件になり、”l”, “m”も、先程はLast Nameの方で検索にひっかかっていたレコードが表示されなくなっていることがわかります。

特定の2列を対象にフィルタリング

次に、first_nameに、last_nameをフィルタリング対象として加える場合を説明します。これは、:filter-included-fieldsの配列パラメータにlast_nameを追加するだけで実現可能です。

<template>
  <div>
    <b-form-input v-model="search_text"></b-form-input>
    <b-table
      :items="items"
      :filter="search_text"
      :filter-included-fields="['first_name', 'last_name']"
    ></b-table>
  </div>
</template>

これも、同じように、数字(8)、文字列(“l”, “m”)で見ていきます。Age列は引き続きフィルタリング対象外なので、数字を入れても何も表示されないところは変わっていませんが、文字列を入力すると、一番最初の全列対象のフィルタリングと同じ結果に変わってきているため、First Name, Last Nameの2列がフィルタリング対象になっていることがわかります。

なお、同じフィルタリングを:filter-ignored-fieldsを使って、以下の用に記載することもできます。上のコードと下のコードの振る舞いは同じになります。

<template>
  <div>
    <b-form-input v-model="search_text"></b-form-input>
    <b-table
      :items="items"
      :filter="search_text"
      :filter-ignored-fields="['age']"
    ></b-table>
  </div>
</template>

Custom Filter

[ads]

ここまで、1つのフィルタリング条件を入力し、1つ、または複数列に対してフィルタリングする例をみてきました。これらのようなシンプルなフィルタリングはBuilt-inフィルタリングで簡単に実装できることがわかりました。ところが、複数条件を使ったフィルタリングなど、少し複雑なフィルタリングを実装しようとすると、Built-inフィルタリングでは実現することができません。そこで必要になるのが、Custom Filterです。

参考:Custom filter function

Custom Filterの基本

まずは、Custom Filterを使って、Built-inフィルタリングと同等のフィルタリングを実装してみます。公式ドキュメントによると、Custom Filterは:filter-functionプロパティを使って、method内の関数を呼び出すことができ、その際に以下の2つの引数を渡す、ということです。

  1. オリジナルアイテム行レコードデータオブジェクト(the original item row record data object)
  2. フィルタープロパティの内容(文字列、正規表現、配列、オブジェクト)

上記に従って、tableFilterというフィルタリング用の関数を作成し、それを:filter-functionで指定することにします。

単一条件でのフィルタ

<template>
  <div>
    <b-form-input v-model="search_text"></b-form-input>
    <b-table
      :items="items"
      :filter="search_text"
      :filter-function="tableFilter"
    ></b-table>
  </div>
</template>
export default {
  data() {
    return {
      items: null,
      searchText: null,
    }
  },
  mounted() {
    this.items = DATA
  },
  methods: {
    tableFilter(row, filterprop) {
      console.log("==== row ====")
      console.log("row: [age, first_name, last_name]", row.age, row.first_name, row.last_name)
      console.log("filterprop:", filterprop)
      console.log("typeof: (row)", typeof row.age, typeof row.first_name, "(filterprop)", typeof filterprop)
      console.log("typeof: (prop)", filterprop)
      return row.first_name.includes(filterprop)
    }
  },
}

tableFilterでは、まず確認のために、何が渡されているかを出力しています。実質的な処理は最後の行のみで、検索用に入力した値が、テーブルの行単位で渡されるデータのなかに含まれていればtrueを、そうでなければfalseを返す、ということをしています。
このコードで同じ用に数字の8を入力してみます。

出力したログを確認してみます。1行ずつ、tableFilterに引数rowとしてデータが渡されていることがわかります。また、検索用に入力した数字はstringとして扱われていますが、rowとして渡されたageのデータはnumberとなっていることがわかります。この値を比較する際は型を合わせるなどひと手間必要そうです。

==== row ====
row: [age, first_name, last_name] 40 Dickerson Macdonald
filterprop: 8
typeof: (row) number string (filterprop) string
typeof: (prop) string
==== row ====
row: [age, first_name, last_name] 21 Larsen Shaw
filterprop: 8
typeof: (row) number string (filterprop) string
typeof: (prop) string
==== row ====
row: [age, first_name, last_name] 89 Geneva Wilson
filterprop: 8
typeof: (row) number string (filterprop) string
typeof: (prop) string
==== row ====
row: [age, first_name, last_name] 38 Jami Carney
filterprop: 8
typeof: (row) number string (filterprop) string
typeof: (prop) string

次に、文字列”l”を入力してみます。2行目の”Larsen”がFirst Nameとして”l”を含んでいるため表示されてほしいところですが、表示されません。

出力したログをみてみます。filterpropとして渡されているのは小文字の”l”で、row.first_nameに含まれているのは大文字の”L”のため、不一致になっています。Built-inフィルタリングではこのあたり”よしな”に処理してくれていますので、Custom Filteringの場合は要注意ですね。

==== row ====
row: [age, first_name, last_name] 40 Dickerson Macdonald
filterprop: l
typeof: (row) number string (filterprop) string
typeof: (prop) string
==== row ====
row: [age, first_name, last_name] 21 Larsen Shaw
filterprop: l
typeof: (row) number string (filterprop) string
typeof: (prop) string
==== row ====
row: [age, first_name, last_name] 89 Geneva Wilson
filterprop: l
typeof: (row) number string (filterprop) string
typeof: (prop) string
==== row ====
row: [age, first_name, last_name] 38 Jami Carney
filterprop: l
typeof: (row) number string (filterprop) string
typeof: (prop) string

大文字小文字の区別なくフィルタリングするようにコードを修正します。(Cosole.log部分は削除しています)

export default {
  data() {
    return {
      items: null,
      search_text: "",
    }
  },
  mounted() {
    this.items = DATA
  },
  methods: {
    tableFilter(row, filterprop) {
      return row.first_name.toLowerCase().includes(filterprop.toLowerCase())
    }
  },
}

修正後のフィルタリングは以下のようになります。無事小文字の”l”でも”Larsen”が表示されるようになりました。大文字”A”を入力しても、小文字の”a”を含むデータが表示されています。

複数列を対象としたフィルタ

次に、Built-inフィルタリングの:filter-included-fieldsと同様、フィルタリング対象列を複数にする場合をみていきます。先程同様、filterpropに渡された検索文字列が、rowで渡される各行のデータの、first_name, last_nameそれぞれに含まれているかをチェックし、いずれかに含まれていればtrueを返すようにtableFilterを変更します。

  methods: {
    tableFilter(row, filterprop) {
      let filter_first_name = row.first_name.toLowerCase().includes(filterprop.toLowerCase())
      let filter_last_name = row.last_name.toLowerCase().includes(filterprop.toLowerCase())
      return filter_first_name || filter_last_name    }
  },

以下の用に、Age列にはフィルタリングが効いていないままである一方、”l”を入力することで、先程は表示されなかったLast Nameに”l/L”が含まれるデータが表示されるようになりました。

複数条件でのフィルタ

ここまででCustom FilterでBuilt-inフィルタリング同等の機能を実装することができるようになりました。ここからがやっと本題となる、複数条件でのフィルタリングを実装していきます。ここでは、以下を実現しようとします。

  • 年齢、名前の2つの検索条件を入力
  • 年齢、名前共に、空欄であれば全件表示とする(空欄データがないため表示しない、ということにはしない)
  • 年齢:入力された値がAge列に含む場合、表示する
  • 名前:入力された値が、First Name列, またはLast Name列に含まれる場合、表示する
  • 年齢と名前はAND条件とする(例:「年齢に”8″を含み、かつ名前に”g”を含む」=> 3行目、89歳 Geneva Wilsonを表示

最初に、複数条件を入力できるようにフォームを追加し、それぞれの値を保持するためのプロパティを用意します。

<template>
  <div>
    <b-form-input
      v-model="age"
      placeholder="age"
    ></b-form-input>
    <b-form-input
      v-model="name"
      placeholder="name"
    ></b-form-input>
    <b-table
      :items="items"
      :filter="search_text"
      :filter-function="tableFilter"
    ></b-table>
  </div>
</template>
export default {
  data() {
    return {
      items: null,
      search_text: "",
      age: "",
      name: "",
    }
  },
  mounted() {
    this.items = DATA
  },
  methods: {
    tableFilter(row, filterprop) {
      let filter_first_name = row.first_name.toLowerCase().includes(filterprop.toLowerCase())
      let filter_last_name = row.last_name.toLowerCase().includes(filterprop.toLowerCase())
      return filter_first_name || filter_last_name    }
  },
}

検索条件が2つ(age, name)になった状態です。v-modelのageもnameは何もフィルタリング機能と関連していないため、これらに入力しても何もフィルタリングされません。

ここで、ageまたはnameプロパティが変更されたことをトリガーにフィルタリングさせることを考えます。computedプロパティを使ってこれを実現してみます。先程まで使っていたデータプロパティのsearch_textの代わりに、computedプロパティのfilterを用意します。これは、単純にデータプロパティのage, nameを配列で返す、というものです。

  computed: {
    filter: function () {
      return [this.age, this.name]
    }
  },

次に、b-table側で:filterプロパティも同様にデータプロパティのsearch_textの代わりに、作成したcomputedプロパティのfilterを指定します。

<template>
  <div>
    <b-form-input
      v-model="age"
      placeholder="age"
    ></b-form-input>
    <b-form-input
      v-model="name"
      placeholder="name"
    ></b-form-input>
    <b-table
      :items="items"
      :filter="filter"
      :filter-function="tableFilter"
    ></b-table>
  </div>
</template>

これで準備完了といきたいところですが、このままだと以下のエラーが出てしまいます。原因は初回、age, nameのいずれかがnullの状態でtoLowerCaseを呼んでいるためです。このエラーへの対応と、要件通り、Ageも対象としたロジックをtableFilterに実装します。

[Vue warn]: Error in getter for watcher “computedItems”:

  methods: {
    tableFilter(row) {
      var filter_age = this.age ? String(row.age).includes(this.age) : true
      var filter_first_name = this.name ? row.first_name.toLowerCase().includes(this.name.toLowerCase()) : true
      var filter_last_name = this.name ? row.last_name.toLowerCase().includes(this.name.toLowerCase()) : true

      return filter_age && (filter_first_name || filter_last_name)
    }
  },

今回は、前回使用した引数で渡されてくるpropfilterの値を使うのではなく、データプロパティの値との比較でロジックを組んでいます。実行結果は以下のとおりです。ageだけ、age+name, nameだけ、いずれでも期待した結果が表示されることがわかります。

まとめ

[ads]

利用シーンはそれなりにありそうながら、中々わかりやすいサンプルが見つけられなかったため、自分自身の整理の意味も込めて投稿しました。BootstrapVueの公式ドキュメントにすべて記載があるといえばありますが、実例が乏しく苦労しましたが、一度わかってしまえばとても分かりやすい仕組みなので、何かの参考にしていただければ幸いです。

最終型のソースコード

<template>
  <div>
    <b-form-input
      v-model="age"
      placeholder="age"
    ></b-form-input>
    <b-form-input
      v-model="name"
      placeholder="name"
    ></b-form-input>
    <b-table
      :items="items"
      :filter="filter"
      :filter-function="tableFilter"
    ></b-table>
  </div>
</template>
<script>
export default {
  data() {
    return {
      items: null,
      age: "",
      name: "",
    }
  },
  mounted() {
    this.items = DATA
  },
  methods: {
    tableFilter(row) {
      var filter_age = this.age ? String(row.age).includes(this.age) : true
      var filter_first_name = this.name ? row.first_name.toLowerCase().includes(this.name.toLowerCase()) : true
      var filter_last_name = this.name ? row.last_name.toLowerCase().includes(this.name.toLowerCase()) : true

      return filter_age && (filter_first_name || filter_last_name)
    }
  },
  computed: {
    filter: function () {
      return [this.age, this.name]
    }
  },
}

const DATA = [
  { age: 40, first_name: 'Dickerson', last_name: 'Macdonald' },
  { age: 21, first_name: 'Larsen', last_name: 'Shaw' },
  { age: 89, first_name: 'Geneva', last_name: 'Wilson' },
  { age: 38, first_name: 'Jami', last_name: 'Carney' }
]

</script>

References

Ads