Quasarのテーブルでセルにボタンを配置する

# Quasarのテーブルでセルにボタンを配置する

Quasarのテーブルでセルにボタンを配置する書き方を確認しました。

![qtable_button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542190/6bb76535-6a1a-1abc-2130-f07108855f4c.png)

## 背景

Quasarのテーブル（[QTable](https://quasar.dev/vue-components/table)）は機能が豊富ですが、公式マニュアルがそれほど親切ではありません。テーブルのセルにボタンを表示する方法をマニュアルから読み解けなかったので、実験してみた結果です。

## ポイント

### 1. v-slot:bodyスロットを使う

v-slot:bodyスロットを使って自力でq-trエレメントとq-tdエレメントをレンダリングします。

### 2. q-tdエレメントでレンダリングする

q-tdエレメントの中にボタンを配置します。

### 3. formatを呼び出す

QTableProp.columnsのformat属性を定義している場合は呼び出す必要があります。

### 4. alignmentを反映する

QTableProp.columnsのalgign属性も無視されるのでstyleに設定します。

### 5. 値を計算する場合にも使える

エレメント内で簡単な計算もできます。

## Vueコンポーネントの全コード

表の中から製品名をクリックして、絞り込み検索するサンプルです。

```vue
<script setup lang="ts">
import { ref } from 'vue';

// テーブルの元データ
const rows1 = [
    { name1: 'Eclair', name2: 'Baked goods', price: 2.50, qty: 16},
    { name1: 'Froyo', name2: 'Yogurt', price: 1.80, qty: 12},
    { name1: 'Ice Cream Sandwich', name2: 'Ice Cream', price: 2.00, qty: 10},
    { name1: 'Doughnut', name2: 'Baked goods', price: 2.00, qty: 7},
    { name1: 'Jelly Bean', name2: 'Jelly', price: 1.50, qty: 12},
    { name1: 'Honeycomb', name2: 'Honey', price: 8.00, qty: 10},
    { name1: 'Cupcake', name2: 'Baked goods', price: 3.00, qty: 17},
    { name1: 'Eclair', name2: 'Baked goods', price: 2.50, qty: 12},
    { name1: 'Ice Cream Sandwich', name2: 'Ice Cream', price: 2.00, qty: 3},
    { name1: 'Honeycomb', name2: 'Honey', price: 8.00, qty: 12},
    { name1: 'Cupcake', name2: 'Baked goods', price: 3.00, qty: 5},
    { name1: 'Froyo', name2: 'Yogurt', price: 1.80, qty: 15},
    { name1: 'Ice Cream Sandwich', name2: 'Ice Cream', price: 2.00, qty: 12},
    { name1: 'Jelly Bean', name2: 'Jelly', price: 1.50, qty: 1},
    { name1: 'Eclair', name2: 'Baked goods', price: 2.50, qty: 12},
    { name1: 'Jelly Bean', name2: 'Jelly', price: 1.50, qty: 4},
    { name1: 'KitKat', name2: 'Chocolate', price: 1.00, qty: 11},
    { name1: 'KitKat', name2: 'Chocolate', price: 1.00, qty: 1},
    { name1: 'Gingerbread', name2: 'Baked goods', price: 1.80, qty: 12},
    { name1: 'Gingerbread', name2: 'Baked goods', price: 1.80, qty: 15},
    { name1: 'Cupcake', name2: 'Baked goods', price: 3.00, qty: 11},
    { name1: 'Honeycomb', name2: 'Honey', price: 8.00, qty: 7},
    { name1: 'Cupcake', name2: 'Baked goods', price: 3.00, qty: 13},
    { name1: 'Eclair', name2: 'Baked goods', price: 2.50, qty: 14},
    { name1: 'Doughnut', name2: 'Baked goods', price: 2.00, qty: 6},
    { name1: 'Honeycomb', name2: 'Honey', price: 8.00, qty: 18},
    { name1: 'Doughnut', name2: 'Baked goods', price: 2.00, qty: 3},
    { name1: 'Lolipop', name2: 'Candy', price: 1.00, qty: 7},
    { name1: 'KitKat', name2: 'Chocolate', price: 1.00, qty: 10},
    { name1: 'Gingerbread', name2: 'Baked goods', price: 1.80, qty: 5},
];

// テーブルの列定義
const columns1 = [
    { name: 'name1', field: 'name1', label: 'Name', sortable: true, align: 'left' },
    { name: 'name2', field: 'name2', label: 'Type', sortable: true, align: 'left' },
    { name: 'price', field: 'price', label: 'Price', sortable: true, format: (val: any) => val.toFixed(2) },
    { name: 'qty', field: 'qty', label: 'Qty' },
    { name: 'subtotal', field: 'subtotal', label: 'Subtotal' },
] as any[];

// 検索結果の行データ
const searchedRows1 = ref(rows1);

// 検索フィールド値
const searchField = ref('');

// 検索処理
const onKeydownSearch = (e: KeyboardEvent) => {
    if (e.key === 'Enter') {
        onClickSearch();
    }
};

// 検索ボタンクリック処理
const onClickSearch = () => {
    if (!searchField.value) {
        searchedRows1.value = rows1;
    } else {
        searchedRows1.value = rows1.filter((row) => row.name1.includes(searchField.value) || row.name2.includes(searchField.value));
    }
}

// セルのボタンクリック処理
const onClickCell = (name: string) => {
    searchField.value = name;
    onClickSearch();
};

</script>

<template>
    <div style="max-width: 40rem;">
        <div class="row q-ma-sm">
            <!-- 検索フィールドとボタン -->
            <q-input outlined v-model="searchField" label="Search" clearable @keydown="onKeydownSearch" />
            <q-btn class="q-ma-sm" size="small" icon="search" round @click="onClickSearch"/>
            <q-space/>
            <div style="display: flex; align-items: last baseline;" class="q-ma-sm">
                Total: <b>{{ searchedRows1.reduce((acc, row) => acc + row.price * row.qty, 0).toFixed(2) }}</b>
            </div>
        </div>
        <!-- テーブル -->
        <q-table :rows="searchedRows1" :columns="columns1" :pagination="{ rowsPerPage: 10}">
            <!--ポイント1. v-slot:bodyスロットを使う-->
            <template v-slot:body="props">
                <q-tr>
                    <!-- ポイント2. q-tdエレメントでレンダリングする / ポイント4. alignmentを反映する -->
                    <q-td v-for="c of props.cols" :key="c.field" :style="{ textAlign: c.align }">
                        <template v-if="c.field === 'name1' || c.field === 'name2'">
                            <q-btn @click="onClickCell(props.row[c.field])">
                                {{ props.row[c.field] }}
                            </q-btn>
                        </template>
                        <template v-else-if="c.field === 'subtotal'">
                            <!-- ポイント5. 値を計算する場合にも使える -->
                            {{ (props.row.price * props.row.qty).toFixed(2) }}
                        </template>
                        <template v-else-if="c.format">
                            <!-- ポイント3. formatを呼び出す -->
                            {{ c.format(props.row[c.field]) }}
                        </template>
                        <template v-else>
                            <!-- 何もしない列をレンダリング -->
                            {{ props.row[c.field] }}
                        </template>
                    </q-td>
                </q-tr>
            </template>
        </q-table>
    </div>
</template>
```

## 確認したバージョン

* vue: **3.5.13**
* quasar: **2.17.7**