Vuetify3のv-data-tableは、まだ開発中でリリースされていない。
→ Vuetify3でリリースされたv-data-tableにアップデートする
仕様が変わる可能性はありますが、Vuetify Labsで使用できるので試してみました。
今回はVuetify2からの移行で、表示・変更・削除する基本的な表示や挙動が対象です。
先に結論、item-class(trのclass指定)、headersのclass(thのclass指定)・cellClass(tdのclass指定)、@dblclick:rowやmobile-breakpoint(モバイルデザイン)は、現段階では動きませんでした。
mobile-breakpoint以外は、項目毎でなく全体をSlots(headers/item)するように修正すれば対応可能ですが、v-data-tableに任せている部分も追加する事になるので、果たして良いのか?
急ぎでなければ、待つ方が得策かもしれません。
vue-infinite-loadingはv3-infinite-loadingとほぼ同じ挙動なので、slotでカスタマイズしていなければ、そのまま使えます。sortとdescが纏められてsort-byで取得でき、個別に設定されなくなったので、別々に設定される事を考慮しなくて良くなったのがいい感じ。
v3-infinite-loading導入と移行
% yarn add -D v3-infinite-loading
(Options API)pages/spaces/index.vue
<InfiniteLoading
v-if="!reloading && space != null && space.current_page < space.total_pages"
:identifier="page"
@infinite="getNextSpacesList"
>
- <div slot="no-more" />
- <div slot="no-results" />
- <div slot="error" slot-scope="{ trigger }">
- 取得できませんでした。
- <v-btn @click="error = false; trigger()">再取得</v-btn>
- </div>
+ <template #spinner>
+ <AppLoading height="10vh" class="mt-4" />
+ </template>
+ <template #complete />
+ <template #error="{ retry }">
+ <AppErrorRetry class="mt-4" @retry="error = false; retry()" />
+ </template>
</InfiniteLoading>
<script>
- import InfiniteLoading from 'vue-infinite-loading'
+ import InfiniteLoading from 'v3-infinite-loading'
+ import AppErrorRetry from '~/components/app/ErrorRetry.vue'
import AppLoading from '~/components/app/Loading.vue'
export default defineNuxtComponent({
components: {
InfiniteLoading,
+ AppErrorRetry,
AppLoading,
methods: {
// 次頁のスペース一覧取得
async getNextSpacesList ($state) {
- if (this.processing || this.error) { return }
+ if (this.error) { return $state.error() } // NOTE: errorになってもloaded(spinnerが表示される)に戻る為
+ if (this.processing) { return }
this.page = this.space.current_page + 1
if (!await this.getSpacesList()) {
if ($state == null) { this.testState = 'error'; return }
$state.error()
style.cssは、slotでカスタマイズする場合は不要です。
API取得に失敗して、$state.error()を呼んでも、直後に@infiniteが呼び出されちゃうので、method側で対応しています。
components/app/Loading.vue
LoadingはAPI取得完了までの初期表示でも使っているので、共通化して見た目を揃えています。
(Options API)
<template>
<div class="text-center">
<v-progress-circular indeterminate color="primary" :size="50" :style="`height: ${height}`" />
</div>
</template>
<script>
export default defineNuxtComponent({
props: {
height: {
type: String,
default: '80vh'
}
}
})
</script>
<style scoped>
.v-progress-circular >>> svg {
height: 50%; /* NOTE: モバイルでヘッダアイコンが左右に動く為 */
}
</style>
モバイルでヘッダアイコンが左右に動く件は、Vuetify2の時から。
components/app/ErrorRetry.vue
複数箇所で使うので、共通化しました。
(Composition API)
<template>
<div class="d-flex align-center justify-center" style="height: 10vh">
<div class="text-grey">
続きを取得できませんでした。
<v-btn id="error_retry_btn" color="secondary" @click="emit('retry')">再取得</v-btn>
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['retry'])
</script>
Vuetify Labs導入
Introduction to Labs — Vuetify
最新を使いたいので、@nextでインストールします。
% yarn add -D vuetify@next
package.json に下記が入りました。
- "vuetify": "^3.3.13"
+ "vuetify": "^3.4.0-alpha.1"
plugins/vuetify.ts
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
+ import * as labsComponents from 'vuetify/labs/components'
import * as directives from 'vuetify/directives'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(createVuetify({
- components,
+ components: {
+ ...components,
+ ...labsComponents
+ },
v-data-table暫定対応
v-data-table-serverとv-data-table-virtualが増えています。
-serverだとソートがサーバーサイドになる。
-virtualは書き換えるので早そうですが、表示がチカチカするので見送り。
(Options API)components/members/Lists.vue
<template>
- <v-data-table
+ <v-data-table-server
v-if="members != null && members.length > 0"
v-model="syncSelectedMembers"
- :sort-by.sync="syncSort"
- :sort-desc.sync="syncDesc"
+ v-model:sort-by="syncSortBy"
:headers="headers"
:items="members"
+ :items-length="members.length"
- item-key="user.code"
- :item-class="itemClass"
:items-per-page="-1"
- hide-default-footer
- mobile-breakpoint="600"
fixed-header
:height="appTableHeight"
- must-sort
- :custom-sort="disableSortItem"
:show-select="admin"
+ :item-value="item => item"
- @dblclick:row="showUpdate"
>
+ <!--
+ :item-class="itemClass" TODO: 背景色が変わらない
+ mobile-breakpoint="600" TODO: モバイルデザインにならない
+ @dblclick:row="showUpdate" TODO: 動かない
+ -->
- </v-data-table>
+ </v-data-table-server>
<script>
export default defineNuxtComponent({
computed: {
syncSortBy: {
get () {
return [{ key: this.sort, order: this.desc ? 'desc' : 'asc' }]
},
set (value) {
if (value.length === 0) {
this.$emit('reload', { sort: this.sort, desc: !this.desc }) // NOTE: 同じ項目で並び順を2回変えると空になる為
} else {
this.$emit('reload', { sort: value[0].key, desc: value[0].order === 'desc' })
}
}
},
methods: {
- disableSortItem (items) {
- return items
- },
text/valueがtitle/keyに変わった
computed: {
headers () {
const result = []
if (this.admin) {
- result.push({ value: 'data-table-select', class: 'pl-3 pr-0', cellClass: 'pl-3 pr-0 py-2' })
+ result.push({ key: 'data-table-select', class: 'pl-3 pr-0', cellClass: 'pl-3 pr-0 py-2' }) // TODO: class/cellClassが効かない
}
for (const item of this.$tm('items.member')) {
- if ((item.required || !this.hiddenItems.includes(item.value)) && (!item.adminOnly || this.admin)) {
+ if ((item.required || !this.hiddenItems.includes(item.key)) && (!item.adminOnly || this.admin)) {
- result.push({ text: item.text, value: item.value, class: 'text-no-wrap', cellClass: 'px-1 py-2' })
+ result.push({ title: item.title, key: item.key, class: 'text-no-wrap', cellClass: 'px-1 py-2' })
}
}
if (result.length > 0) { result[result.length - 1].cellClass = 'pl-1 pr-4 py-2' } // NOTE: スクロールバーに被らないようにする為
return result
},
itemがitem.rawに変わった
<template #[`item.user.name`]="{ item }">
<div class="ml-1">
- <UsersAvatar :user="item.user" />
+ <UsersAvatar :user="item.raw.user" />
methods: {
showUpdate (event, { item }) {
- if (!this.admin || item.user.code === this.$auth.user.code) { return }
+ if (!this.admin || item.raw.user.code === this.$auth.user.code) { return }
- this.$emit('showUpdate', item)
+ this.$emit('showUpdate', item.raw)
headerがcolumnに変わった。ソートアイコンが出なくなった
- <template #[`header.user.email`]="{ header }">
+ <template #[`column.user.email`]="{ column, getSortIcon }">
- {{ header.text }}
+ {{ column.title }}
<OnlyIcon power="admin" />
+ <v-icon class="v-data-table-header__sort-icon">{{ getSortIcon(column) }}</v-icon>
</template>
slot内のaリンクに色や下線が付かなくなった
通常のaリンクに付く、これを追加しました。
a:-webkit-any-link {
color: -webkit-link;
cursor: pointer;
text-decoration: underline;
}
<template #[`item.power`]="{ item }">
<a
class="text-no-wrap"
+ style="color: -webkit-link; cursor: pointer; text-decoration: underline"
hide-default-footerが効かない
footerのSlotsがなさそうなので、CSSで対応しました。
<style scoped>
+ .v-data-table >>> .v-data-table-footer {
+ display: none; /* NOTE: hide-default-footerが効かない為 */
+ }
hide-default-headerが効かない
こちらはheadersのSlotsに空を渡せばOK。
components/spaces/Lists.vue
<v-data-table-server
>
+ <template #headers /><!-- NOTE: hide-default-headerが効かない為 -->
disable-sortが効かない
すべて使わないは指定できなそうでしたが、
headersに「sortable: false」を追加すれば個別指定が可能。
components/invitations/Lists.vue
- result.push({ title: item.title, key: item.key })
+ result.push({ title: item.title, key: item.key, sortable: false })
今回のコミット内容
origin#507 Vuetify Labs・v3-infinite-loading導入、v-data-table暫定対応、必須・任意ラベル変更、リファクタ
“Vuetify Labsとv3-infinite-loadingでv-data-tableを暫定対応する” に対して1件のコメントがあります。