Статьи серии
Данная статья появилась на свет по двум причинам. Во-первых ряд читателей приходили с вопросами о создании компонента-таблицы на Vue. Во-вторых в прошлой статье мы подошли к продвинутому использованию слотов. И такой компонент послужил бы неплохим кандидатом для применения данных техник. Сами техники эффективного расширения компонентов слотами будут продемонстрированы в следующей статье. А в этой будет описано создание начальной версии таблицы, которую потом можно будет развивать.
Содержание
- Почему Vue 3?
- Описание результата
- Данные с серверного API
- Описание работы примера
- Как можно развить компонент далее
- Готовые альтернативы
- Дальнейшие планы
Почему Vue 3?
На данный момент Vue версии 2 с Options API уже считается сильно устаревшим. Переход на Composition API возможен и в последних релизах второй версии. Если вы все еще не используете новый API, то рекомендую начать как можно скорее. Обновиться максимально в рамках версии 2, а, если возможно, то перейти на версию 3. Чтобы не использовать устаревшие подходы данная статья будет содержать примеры на базе Composition API. В идеале нужен бы был еще TypeScript. Но для расширения круга возможных читателей пока будет использоваться JavaScript.
Описание результата
И так создаваемый компонент будет представлять из себя таблицу с данными. Данные будут разбиты на страницы (порции выборки). В таблицу будет загружена только одна страница данных. В нижней части таблицы (футере) будет переключатель страниц. В верхней части таблицы можно будет управлять сортировкой кликая по заголовку столбца.
Данные с серверного API
Фокусом статьи будет front end. Поэтому данные мы будем получать готовые. Предлагается использовать сервис JSONPlaceholder, откуда получим тестовые строки. Используется маршрут https://jsonplaceholder.typicode.com/posts
, который выдает данные по сообщениям (posts) с возможностью указания номера страницы (параметр _page
), числа записей на странице (_limit
) и параметров упорядочивания (сортировки) данных (_sort
, _order
).
Описание работы примера
Код примера таблицы на Vue 3 готовый к запуску можете загрузить из репозитория. В нем содержатся следующие файлы:
src/components/PostDataTable.vue
– собственно, компонент-таблицаsrc/api/posts.js
– обращение к API сервераsrc/api/helpers.js
– преобразование данных компонента в параметры для API сервера (и обратно)src/assets
– Общие CSS стили приложенияsrc/App.vue
– компонент приложения
Как работает компонент таблицы пояснено с помощью комментариев в коде.
<template>
<table class="data-table">
<thead>
<tr :class="{ 'data-table__row_loading': loading }">
<!-- Создание заголовка для каждой колонки -->
<th
v-for="field in headers"
:key="field.key"
:style="{ width: field.width, minWidth: field.width }"
:class="{ 'data-table__th-sortable': field.sortable }"
@click="handleToggleSortAction(field)"
>
<!-- Текст в заголовке с название столбца -->
{{ field.title }}
<!-- Пиктограмма-стрелка для отображения направления сортировки -->
<template v-if="options.sortBy === field.key">
<svg-icon
type="mdi"
class="icon"
v-if="options.sortDir === 'asc'"
:path="ascSortIcon"
></svg-icon>
<svg-icon
type="mdi"
class="icon"
v-if="options.sortDir === 'desc'"
:path="descSortIcon"
></svg-icon>
</template>
</th>
</tr>
<!-- Отображение анимации загрузки -->
<tr v-if="loading" class="data-datatable__progress">
<td :colspan="headers.length">
<div class="progress-bar">
<div class="progress-bar__value"></div>
</div>
</td>
</tr>
</thead>
<tbody>
<!-- Создание строки для каждой записи данных -->
<tr v-for="item in pageItems" :key="item" :class="{ 'data-table__row_loading': loading }">
<!-- Создание ячейки с данными в каждой строке -->
<td v-for="field in headers" :key="field.key" :style="{ textAlign: field.align }">
<!-- Содержимое ячейки -->
{{ item[field.key] }}
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td :colspan="headers.length">
<div class="data-table__footer-content">
<!-- Кнопка перехода на предыдущую страницу -->
<button
class="icon-btn"
:disabled="options.page === 1"
@click="handlePreviousPageAction"
>
<svg-icon type="mdi" :path="previousIcon"></svg-icon>
</button>
page #{{ options.page }} from {{ pagesCount }}
<!-- Кнопка перехода на следующую страницу -->
<button
class="icon-btn"
:disabled="options.page >= pagesCount"
@click="handleNextPageAction"
>
<svg-icon type="mdi" :path="nextIcon"></svg-icon>
</button>
</div>
</td>
</tr>
</tfoot>
</table>
</template>
<script setup>
import { getPosts } from '@/api/posts.js'
import { computed, onMounted, reactive, ref } from 'vue'
import SvgIcon from '@jamescoyle/vue-icon'
import {
mdiArrowLeft as previousIcon,
mdiArrowRight as nextIcon,
mdiArrowDown as ascSortIcon,
mdiArrowUp as descSortIcon
} from '@mdi/js'
import {
covertDataTableOptionsToQueryParams,
convertApiResponceToDataTableOptions
} from '@/api/helpers.js'
// Строки текущей страницы
const pageItems = ref([])
// Флаг активности процесса загрузки данных
const loading = ref(true)
// Общее число строк на сервере
const totalItemsCount = ref(0)
/**
* Настройки колонок
*/
const headers = [
{
title: 'ID',
align: 'end',
sortable: true,
key: 'id',
width: '5em'
},
{
title: 'Title',
align: 'start',
sortable: true,
key: 'title',
width: '100%'
}
]
const options = reactive({
page: 1, // Номер страницы
itemsPerPage: 10, // Число строк на страницу
sortBy: 'id', // Имя столбца для сортировки
sortDir: 'asc' // Порядок сортировки
})
// Число страниц
const pagesCount = computed(() => Math.ceil(totalItemsCount.value / options.itemsPerPage))
// Обработка нажатия кнопки перехода на следующую страницу
function handleNextPageAction() {
options.page++
loadData()
}
// Обработка нажатия кнопки перехода на предыдущую страницу
function handlePreviousPageAction() {
options.page--
loadData()
}
// Обработка нажатия заголовка столбца для сортировки
function handleToggleSortAction(filed) {
if (!filed.sortable) {
return
}
/**
* 3 режима сортировки: возрастание, убывание и отсутствие сортировки
*/
const filedName = filed.key
if (options.sortBy === filedName) {
if (options.sortDir === 'asc') {
options.sortDir = 'desc'
} else {
options.sortBy = null
}
} else {
options.sortBy = filedName
options.sortDir = 'asc'
}
loadData()
}
// Загрузка данных для текущей страницы с сервера
async function loadData() {
loading.value = true
const params = covertDataTableOptionsToQueryParams(options)
const data = convertApiResponceToDataTableOptions(await getPosts(params))
pageItems.value = data.items
totalItemsCount.value = data.total
loading.value = false
}
onMounted(loadData)
</script>
<style>
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table td,
.data-table th {
border: 1px solid #ddd;
padding: 8px;
}
.data-table tbody tr:nth-child(even) {
background-color: #f2f2f2;
}
.data-table tbody tr:hover {
background-color: #ddd;
}
.data-table th {
padding-top: 12px;
padding-bottom: 12px;
text-align: center;
}
.data-table__row_loading {
opacity: 0.5;
}
.data-datatable__progress td {
padding: 0;
position: relative;
border: none;
}
.data-datatable__progress .progress-bar {
position: absolute;
left: 0;
bottom: 0;
}
.data-table__footer-content {
display: flex;
align-items: center;
justify-content: right;
}
.data-table__th-sortable {
cursor: pointer;
}
.data-table__th-sortable .icon {
vertical-align: middle;
}
</style>
import axios from 'axios'
export async function getPosts(params) {
return await axios.get('https://jsonplaceholder.typicode.com/posts', { params })
}
// Преобразование данных компонента в параметры API
export function covertDataTableOptionsToQueryParams(opts) {
const result = {
_limit: opts.itemsPerPage,
_page: opts.page
}
if (opts.sortBy) {
result._sort = opts.sortBy
result._order = opts.sortDir ? opts.sortDir : 'asc'
}
return result
}
// Преобразование результата API в данные для компонента
export function convertApiResponceToDataTableOptions(data) {
return { items: data.data, total: data.headers['x-total-count'] }
}
<template>
<h1>Vue3 DataTable</h1>
<PostDataTable></PostDataTable>
</template>
<script setup>
import PostDataTable from '@/components/PostDataTable.vue'
</script>
Как можно развить компонент далее
Сейчас компонент содержит работу со страницами и сортировкой. Конечно, это нельзя назвать широкой функциональностью. Но данный пример задает базу, которую можно развивать. Можно дать возможность пользователю задавать число строк на странице, переходить к первой и последней странице. Дать возможность программисту вставлять шаблоны в ячейки таблицы. В этих шаблонах могут быть кнопки для действий с данными. Простор для развития имеется не малый.
Готовые альтернативы
Чтобы не изобретать велосипед, можно воспользоваться готовыми компонентами. Для этого можно взять какой-либо UI-фреймворк для Vue. В одной из предыдущих статьей упоминался неплохой вариант – Vuetufy. Сейчас уже развивается 3я версия этого пакета.
Дальнейшие планы
В следующей статье мы поразмышляем над тем как добавить этому компоненту возможность использовать шаблоны в ячейках. Эти шаблоны могут содержать интерактивные элементы. Например, кнопки. Мы реализуем эту возможность, чтобы попрактиковаться в использовании слотов.
Если у вас есть какие-либо вопросы, пожелания или замечания – пожалуйста, пишите.
Чтобы не пропустить очередную статью вы можете подписаться на канал.
А как этот компонент подключить?
Не работает – как подключить?
В итоге пишет ошибку
Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “../”.
и не хочет работать
Добрый день
На всякий случай обращу Ваше внимание, что код примера есть на GitHub. Ссылка – в статье. Можно в репозитории посмотреть как что подключается.
Например, компонент таблицы подключается здесь – https://github.com/itelmenko/myblog/blob/main/examples/vue-data-table/src/App.vue
При том сам компонент расположен тут https://github.com/itelmenko/myblog/blob/main/examples/vue-data-table/src/components/PostDataTable.vue
А как вы подключаете? Какая у вас версия Vue? В примере – 3.
Эта фигня на Node.js о чём в статье нис лова не сказано, зря время потратил.
Мне нужно было просто vue без Node.jsи всяких там сборщиков которые три километра непонятно чего за собой тянут ради простой таблички
В последнее время при разработке js-приложений ставят требующиеся пакеты через npm. Так сказать, общепринятый путь. Поэтому об этом явно не говорится. Да и код проекта есть. Vue именно так и подключен.
А вы используете Vue, подключая готовую сборку фреймворка Vue прямо на страничку? На мой взгляд, это не лучшая практика. Так уже редко кто делает. Но в каких-то случаях, наверное, пригодится.
Огромное спасибо за такой прекрасный пост и компонент. Отличная база, есть от чего отталкиваться. Компонент как раз кстати.
Рад, что статья приносит пользу. Спасибо за обратную связь.