Nuxt.js
TailwindCSS
TypeScript
概要
- 業務システムだとテーブルって頻出コンポーネントだけど極力シンプルなものを作りたい
- Nuxt.js v3とTailwindCSSを使ったデータテーブルのコンポーネントを実装する
という話。
はじめに
TailwindCSSはここ最近勢いのあるCSSフレームワークなので触っておこうと思い、とある現場でNuxt2からNuxt3へ移行するシステムがあったので導入してみました。
Nuxt2のものはBootstrapを採用していたのですが、ついでに入れていたBootstrap VueがVue3へのアップグレードと共に使い物にならなくなった(破壊的な変更が多すぎて素直にアップグレードできない)という経緯があり、
「この際だからNuxt3にするついでにTailwindCSSにして作り直してしまえ」という判断をし、今に至ります。
ただ2024年6月12日時点で、もう次のNuxt4がリリースされるという情報を同僚から聞いてガン萎えしてます。
あとTailwindCSSのUIコンポーネントはググると色んなライブラリが出てくるんですが、極力そういうライブラリは使いたくないなという思いがあり、コンポーネントを自作することにしました(Bootstrap Vueで痛い目を見たから)。
とりあえず完成品
StackBlitzに完成品のコードを置いておきます。
解説とか面倒臭いって人はこちらだけどうぞ。
https://stackblitz.com/edit/tailwind-datatable?file=components%2FDataTable.vue
package.json
を見てもらえれば分かりますが、使用しているフレームワーク・ライブラリは以下の3つだけです。
- Nuxt.js 3.11.2
- TailwindCSS 3.4.4
- Lodash 4.17.21
ね、だいぶシンプルでしょ?
ほな中身の話でもしようか
ざっくり概要
使い方は巷にあるフレームワークと同じような味付けにしています。
ヘッダーの定義とボディのデータをコンポーネントに渡したら、テーブルを描画してくれるって具合です。
あとソート機能を実装してあるので、ヘッダー定義の中でソートをONにするか定義します。
また文字列は全て中央寄せで統一してあるので、列によっては右寄せにしたかったり、複数の列の組み合わせで何か描画したかったり、みたいな場合は <template>
ブロックで中身を差し替えられるようにしています。
ソート機能について
ソート機能部分の実装は以下になります。
const sortBy = ref<string | null>(null);
const sortOrder = ref<'asc' | 'desc'>('asc');
const sortedRows = computed((): T[] => {
if (!sortBy.value) {
return items.value;
}
return _.orderBy(items.value, [sortBy.value], [sortOrder.value]);
});
const sortTable = (column: Column) => {
if (sortBy.value === column.key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
sortBy.value = column.key;
sortOrder.value = 'asc';
}
};
ポイントとしてはコンポーネントのインプットとして defineModel
で受けている items
をそのままテーブルの v-for
に食わせるのではなく、 computed
でソートした sortedRows
を食わせることにあります。
ソート可能と定義した列のヘッダーをクリックすると sortTable
関数が実行され、ソートする列とソート順を表すリアクティブな変数である sortBy
/ sortOrder
を変更しにいきます。
そして sortedRows
は computed
で定義しているので、リアクティブな変数の変化を検知してデータのソートを実行する、という仕掛けになっています。
名前付きスロットによるカスタマイズ性
テーブルのボディ部分の実装は以下になります。
<template>
<!-- 省略 -->
<tr
v-for="(item, i) in sortedRows"
:key="`row-${i}}`"
class="even:bg-blue-100 odd:bg-white hover:bg-gray-100"
>
<td
v-for="col in props.header"
:key="`row-${i}-${col.key}`"
class="border border-gray-300 text-center py-2.5 px-3.5 text-base"
>
<slot :name="col.key" :row="item">
{{ item[`${col.key}`] }}
</slot>
</td>
</tr>
<!-- 省略 -->
</template>
ポイントとしてはヘッダーに定義した key
を使って名前付きスロットにしているところにあります。
こうすることで何も手を加えなければテキストを中央寄せで表示するだけなんですが、コンポーネントの使用側でカスタマイズできる柔軟性を持たせています。
例えばコンポーネントの使用側で以下のようすると
<template>
<DataTable :header="header" v-model:items="items">
<!-- #xxx はヘッダーに定義したkey -->
<template #name="{ row }">
{{ `${row.firstName} - ${row.lastName}` }}
</template>
</DataTable>
</template>
のように特定の列だけ、実装を差し替えることができます。
ジェネリクスによる型推論
コンポーネントの <script>
ブロックの宣言部分も合わせて見てみると、以下のような実装になっています。
<script setup lang="ts" generic="T extends Record<string, any>">
// 省略
const items = defineModel<T[]>('items', {
required: true,
});
// 省略
</script>
ポイントとしては defineModel
として受けている items
の型をジェネリクスを使って潰しているところにあります。
シンプルにこのコンポーネントにデータを渡すだけだと気付かないのですが、1つ手前で解説した列を個別にカスタマイズする時にジェネリクスが威力を発揮してきます。
試しに1つ手前の解説と同じように適当な列をコンポーネントの使用側でカスタマイズしようとしてみてください。
するとあら不思議、 row.
と打ったらプロパティのサジェストが出てきませんか?
これはジェネリクスによって型が実行時に決まるような形になっているので、TypeScriptが row
という変数がどのような型であるかいい感じに型推論をしてくれていることによるものです。
これによってTypeScriptの型の恩恵を受けつつ、柔軟なカスタマイズが可能になるわけです。
まとめ
TailwindCSSのUIコンポーネントも割と簡単なものであれば、オープンソースとして公開しているものがたくさんあるので、基本的にはそれらをコピペしつつ必要に応じてクラスを変えてカスタマイズしていくのがいいのかなとは思っています。
ただ今回実装したデータテーブルのコンポーネントなんかは、TailwindCSSをベースにしたフレームワークやライブラリみたいなのを追加で入れないといけなかったり、ものによっては月額ライセンスを払わないと使えない、なんてこともあったので自作してみました。