Таблица на Vue 3. Создание компонента

Эта статья является частью 2 из 7 в серии Vue. Повторное использование кода

Данная статья появилась на свет по двум причинам. Во-первых ряд читателей приходили с вопросами о создании компонента-таблицы на Vue. Во-вторых в прошлой статье мы подошли к продвинутому использованию слотов. И такой компонент послужил бы неплохим кандидатом для применения данных техник. Сами техники эффективного расширения компонентов слотами будут продемонстрированы в следующей статье. А в этой будет описано создание начальной версии таблицы, которую потом можно будет развивать.

Содержание

Почему Vue 3?

На данный момент Vue версии 2 с Options API уже считается сильно устаревшим. Переход на Composition API возможен и в последних релизах второй версии. Если вы все еще не используете новый API, то рекомендую начать как можно скорее. Обновиться максимально в рамках версии 2, а, если возможно, то перейти на версию 3. Чтобы не использовать устаревшие подходы данная статья будет содержать примеры на базе Composition API. В идеале нужен бы был еще TypeScript. Но для расширения круга возможных читателей пока будет использоваться JavaScript.

Описание результата

И так создаваемый компонент будет представлять из себя таблицу с данными. Данные будут разбиты на страницы (порции выборки). В таблицу будет загружена только одна страница данных. В нижней части таблицы (футере) будет переключатель страниц. В верхней части таблицы можно будет управлять сортировкой кликая по заголовку столбца.

Внешний вид компонента-таблицы на Vue3

Данные с серверного 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я версия этого пакета.

Дальнейшие планы

В следующей статье мы поразмышляем над тем как добавить этому компоненту возможность использовать шаблоны в ячейках. Эти шаблоны могут содержать интерактивные элементы. Например, кнопки. Мы реализуем эту возможность, чтобы попрактиковаться в использовании слотов.

Если у вас есть какие-либо вопросы, пожелания или замечания – пожалуйста, пишите.

Чтобы не пропустить очередную статью вы можете подписаться на канал.

5 2 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
8 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Nika SR
Участник
Nika SR
4 месяцев назад

А как этот компонент подключить?

Nika SR
Участник
Nika SR
4 месяцев назад

Не работает – как подключить?

Nika SR
Участник
Nika SR
4 месяцев назад

В итоге пишет ошибку

Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “../”.
и не хочет работать

Nika SR
Участник
Nika SR
4 месяцев назад

Эта фигня на Node.js о чём в статье нис лова не сказано, зря время потратил.
Мне нужно было просто vue без Node.jsи всяких там сборщиков которые три километра непонятно чего за собой тянут ради простой таблички

Виктор
Гость
Виктор
3 месяцев назад

Огромное спасибо за такой прекрасный пост и компонент. Отличная база, есть от чего отталкиваться. Компонент как раз кстати.