最近はAngularからVue.jsに浮気中の8amjpです。Vue.jsめっちゃ楽しいです。
v-data-tableコンポーネントを使う
さて、下記のようなデータがあるとします。
(ちなみにこのデータ、Zabbix APIで取得できるホスト一覧データから抜粋したものです)
[
{
"hostid": "1",
"host": "Server 1",
"description": "サーバ1号機",
"parentTemplates": [
{
"name": "Windows Servers",
"templateid": "1"
},
{
"name": "Web Servers",
"templateid": "2"
}
],
"interfaces": [
{
"interfaceid": "1",
"ip": "192.168.1.1",
"port": "80",
}
]
},
{
....
よくあるタイプのJSONデータですね。parentTemplates
やinterfaces
といった項目が配列になっており、その中に複数の子オブジェクトが格納されている……という階層構造になっています。
さて、このデータを、Vuetifyのv-data-tableコンポーネントを使って表示すると、こんな感じになると思います。
<template>
<v-data-table :headers="headers" :items="items" :search="search">
<template v-slot:top>
<v-text-field v-model="search" append-icon="mdi-magnify" label="Search" single-line/>
</template>
<template v-slot:item.parentTemplates="{ item }">
<div v-for="i in item.parentTemplates" :key="i.templateid">{{ i.name }}</div>
</template>
<template v-slot:item.interfaces="{ item }">
<div v-for="i in item.interfaces" :key="i.interfaceid">{{ i.ip }}:{{ i.port }}</div>
</template>
</v-data-table>
</template>
<script>
export default {
props: ['items'],
data () {
return {
search: '',
headers: [
{
text: 'ホスト名',
value: 'host',
},
{
text: '説明',
value: 'description',
},
{
text: 'テンプレート',
value: 'parentTemplates',
},
{
text: 'インターフェース',
value: 'interfaces',
},
]
}
},
}
</script>
階層構造のデータは、そのままだと表示できないので、item.<name>
スロットを使ってv-for
ですべてのデータを表示したりします。便利ですねー。
カスタムフィルタを使う
さて、ここから本題。
v-data-table
は、データを簡単にフィルタリングするためにsearch
プロパティが用意されています。
バインドされたテキストフィールドに文字を入力すると、その文字列が含まれる行のみをフィルタリングできます。よくある機能ではありますが、最初から用意されていると実装もとても簡単。
ところが、です。前述のデータで言うと、host
やdescription
などの項目はフィルタリングできるのですが、parentTemplates
やinterfaces
の項目に含まれるデータはフィルタリングができません。例えば、「Windows」というキーワードを入力してもヒットしません。
理由はもちろん、文字列じゃないから。さすがに配列の中身までは検索してくれません。
こういう時こそカスタムフィルタの出番です。
標準のフィルタを確認する
まずは、標準のフィルタがどういう動きをしているのか確認します。
function defaultFilter(value, search, item) {
return value != null &&
search != null &&
typeof value !== 'boolean' &&
value.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;
}
この中のvalue
という変数に、JSONデータの「値」の部分が入るんですけど。
値が配列の場合でも、うまい具合に文字列に変換して渡すことで、なんとかなりそうな感じがします。
では実際にやってみましょう。v-data-table
のcustom-filter
プロパティに独自のメソッドを追加することで、動作を上書きすることができるんです。
まずはcustom-filter
プロパティを追加して、独自のメソッドを指定します。
<v-data-table :headers="headers" :items="items" :search="search" :custom-filter="customFilter">
で、追加したメソッドで、フィルタリングの動作を上書きします。こんな感じで。
methods: {
customFilter (value, search) {
return value != null &&
search != null &&
typeof value !== 'boolean' &&
// value.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;
(typeof value === 'object' ? value.map(v => v.name).join('\t') : value)
.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;
},
},
まず、変数value
(=JSONデータの「値」)の型をtypeof
演算子で判定します。
で、object
だった場合は配列とみなし、.map
メソッドで配列内のname
プロパティのみを抽出して、タブ文字で連結して単一の文字列にしています。
このように、単一の文字列にしてしまうことで、検索ができるようになります。
(当然ながら、typeof
でobject
が返ってきても、配列じゃない場合もあるんですけど。その辺は使用するデータに応じて各自いい感じに処理を分岐してくださいね(丸投げ))
ついでに、interfaces
配列内のip
プロパティも検索対象に含めたい場合は、こんな感じでどうでしょうか。
(typeof value === 'object' ? value.map(v => v.name || v.ip).join('\t') : value)
.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;
完成
完成品はこちらです。わーい。
<template>
<v-data-table :headers="headers" :items="items" :search="search" :custom-filter="customFilter">
<template v-slot:top>
<v-text-field v-model="search" append-icon="mdi-magnify" label="Search" single-line/>
</template>
<template v-slot:item.parentTemplates="{ item }">
<div v-for="i in item.parentTemplates" :key="i.templateid">{{ i.name }}</div>
</template>
<template v-slot:item.interfaces="{ item }">
<div v-for="i in item.interfaces" :key="i.interfaceid">{{ i.ip }}:{{ i.port }}</div>
</template>
</v-data-table>
</template>
<script>
export default {
props: ['items'],
data () {
return {
search: '',
headers: [
{
text: 'ホスト名',
value: 'host',
},
{
text: '説明',
value: 'description',
},
{
text: 'テンプレート',
value: 'parentTemplates',
},
{
text: 'インターフェース',
value: 'interfaces',
},
]
}
},
methods: {
customFilter (value, search) {
return value != null &&
search != null &&
typeof value !== 'boolean' &&
(typeof value === 'object' ? value.map(v => v.name || v.ip).join('\t') : value)
.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;
},
},
}
</script>