Vue: таблица с прилипающей шапкой и горизонтальным скролом

от автора

в
Время чтения: 3 мин.

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


Постановка задачи

Нам нужно создать таблицу, которая:

  1. Имеет фиксированную шапку, которая остается видимой при прокрутке.
  2. Поддерживает горизонтальный скролл для тела таблицы.
  3. Автоматически синхронизирует ширину колонок шапки и тела таблицы.
  4. Адаптируется к изменению данных и размеров таблицы.

Реализация

Шаг 1: Структура компонента

Начнем с создания структуры компонента. Мы разделим таблицу на две части: шапку (table-header) и тело (table-body). Шапка будет фиксированной, а тело — прокручиваемым.

<template>
  <div class="table-container">
    <div class="table-header" ref="tableHeader">
      <table style="table-layout: fixed">
        <thead>
          <tr>
            <th v-for="(header, index) in headers" :key="index" :style="{width: columnWidths[index] + 'px'}">
              {{ header }}
            </th>
          </tr>
        </thead>
      </table>
    </div>
    <div class="table-body" @scroll="syncScroll">
      <table>
        <thead style="visibility: collapse">
          <tr ref="tableCols">
            <th>
              {{ header }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, rowIndex) in rows" :key="rowIndex">
            <td v-for="(cell, cellIndex) in row" :key="cellIndex">
              {{ cell }}
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

Шаг 2: Логика компонента

Теперь добавим логику для синхронизации ширины колонок и обработки изменений данных.

<script>
export default {
  data() {
    return {
      ready: false,
      headers: ['Header 1', 'Header 2', 'Header 3', 'Header 4', 'Header 5'],
      rows: [],
      columnWidths: [200, 200, 200, 200, 200], // Начальные ширины колонок
    }
  },
  watch: {
    rows() {
      if (this.ready) {
        this.$nextTick(() => {
          this.syncColumnWidths()
        })
      }
    },
    ready() {
      this.$nextTick(() => {
        this.syncColumnWidths()
      })
    },
  },
  mounted() {
    this.ready = true
    this.observer = new ResizeObserver(() => this.tableResized())
    this.observer.observe(this.$el)
  },
  beforeDestroy() {
    this.observer.disconnect()
    this.ready = false
  },
  methods: {
    syncScroll(event) {
      this.$refs.tableHeader.scrollLeft = event.target.scrollLeft
    },
    syncColumnWidths() {
      this.columnWidths = [...this.$refs.tableCols.children].map((col) => col.offsetWidth)
    },
    tableResized() {
      requestAnimationFrame(() => this.syncColumnWidths())
    },
  },
  // Следующий хук добавлен для демонстрации
  created() {
    // Генерация строковых данных для таблицы
    function generateRows(rowsCount, colsCount, multiplier) {
      return [...Array(rowsCount).keys()].map((i) => {
        return [...Array(colsCount).keys()].map((j) => {
          return `Row ${i + 1} Cell ${j + 1} Paragraph ${Math.pow(10, j + multiplier)}`
        })
      })
    }

    // Асинхронное добавление данных
    setTimeout(() => {
      this.rows = [...this.rows, ...generateRows(50, 5, 0)]
    }, 200)
    setTimeout(() => {
      this.rows = [...this.rows, ...generateRows(50, 5, 2)]
    }, 1000)
  },
}
</script>

Шаг 3: Стилизация

Добавим стили для фиксированной шапки и прокручиваемого тела таблицы.

<style scoped>
.table-container {
  width: 100%;
}

.table-header {
  position: sticky;
  top: 0;
  z-index: 1;
  overflow: hidden;
  background-color: #fff;
}

.table-body {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  white-space: nowrap;
}

th,
td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
  box-sizing: border-box;
}
</style>

Как это работает

  1. Фиксированная шапка:
    • Шапка таблицы (table-header) прилипает к верхней части экрана благодаря position: sticky.
    • Горизонтальный скролл шапки синхронизируется с телом таблицы через метод syncScroll.
  2. Синхронизация ширины колонок:
    • В теле таблицы находится скрытая шапка (<thead> с visibility: collapse), которая используется для измерения ширины колонок.
    • Метод syncColumnWidths обновляет массив columnWidths на основе ширины колонок скрытой шапки.
    • Ширина колонок применяется к шапке и ячейкам таблицы через :style="{ width: columnWidths[index] + 'px' }".
  3. Реактивность:
    • При изменении данных (rows) или флага ready вызывается syncColumnWidths, чтобы обновить ширину колонок.
    • ResizeObserver отслеживает изменения размеров таблицы и вызывает tableResized, который обновляет ширину колонок.

Заключение

Этот подход позволяет создать таблицу с фиксированной шапкой и синхронизацией ширины колонок, которая адаптируется к изменению данных и размеров таблицы. Использование ResizeObserver и requestAnimationFrame делает решение производительным и удобным для поддержки.

Такой компонент может быть полезен в приложениях, где требуется отображение больших объемов данных с удобной навигацией.


Демонстрация


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Сколько будет 4 + 6?