В статье я покажу стандартную табличную разметку, какие у неё есть альтернативы. Дам пример собственной таблицы и разметки, а также опишу общие моменты её реализации.


Стандартная HTML4 таблица


Когда появилась необходимость в HTML разметке показывать таблицы — изобрели тег <table>.
Что же даёт нам таблица в браузере? Вот несколько основных "фич":



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


В первом примере ширина всей таблицы (примерно) = 387px, колонки Company = 206px, колонки Contact = 115px.


В процентах Company = 206px/387px * 100% = 53%, Contact = 115px/387px * 100% = 30%.


Теперь когда содержимое таблицы растянулось, ширина всей таблицы (примерно на моем экране) = 1836px, колонки Company = 982px, колонки Contact = 551px.


В процентах Company = 982px/1836px * 100% = 53%, Contact = 551px/1836px * 100% = 30%.



Можно "дожать" таблицу указав ей CSS свойство table-layout: fixed. Описание к свойству.


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


Если мы не указали ширину столбцов, тогда при "сломанной" таблице, ширина каждого столбца = вся ширина / количество столбцов.


  • Схлопывание (наложение) границ ячеек/столбцов border-collapse: collapse, если мы указали границы для ячеек. Т.е. в местах соприкосновения ячеек, не будет двойных граничных линий.


  • Группировка шапки. Реализуется атрибутами colspan, rowspan.

Использование стандартной таблицы


Во всех вышеприведенный примерах в разметке таблицы я использовал сокращенную разметку:


Cокращенная разметка
<table>
  <tr>
    <th>Header 1</th>
    <th>Header 2</th>
  </tr>
  <tr>
    <td>1.1</td>
    <td>1.2</td>
  </tr>
  <tr>
    <td>2.1</td>
    <td>2.2</td>
  </tr>
</table>

Однако можно использовать "каноничную" разметку:


Каноничная разметка
<table>
  <thead>
    <tr>
      <th>Header 1</th>
      <th>Header 2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1.1</td>
      <td>1.2</td>
    </tr>
    <tr>
      <td>2.1</td>
      <td>2.2</td>
    </tr>
  </tbody>
</table>

Если нужна таблица без шапки и в то же время нам необходимо контроллировать ширину столбцов:


Разметка без шапки
<table>
  <tbody>
    <colgroup>
      <col width="100px"></col>
      <col width="150px"></col>
    </colgroup>
    <tr>
      <td>1.1</td>
      <td>1.2</td>
    </tr>
    <tr>
      <td>2.1</td>
      <td>2.2</td>
    </tr>
  </tbody>
</table>

Чаще всего нам в разметке необходимо получить следующее. У нас есть некий контейнер с заданной шириной или с заданной максимальной шириной. Внутри него мы хотим вписать таблицу.


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


Но ни в коем случае мы не хотим, чтобы таблица сделала наш контейнер шире чем мы задали.


По этой ссылке можно уведеть контейнер с таблицей в действии. Если мы будем сужать контейнер, то в тот момент, когда таблица уже больше не сможет сужаться — появиться скролл.


Подстройка таблицы


Задание ширины таблицы и столбцов


Первая дилемма с которой сталкиваются фронт-энд разработчики — это задавать или не задавать ширину столбцов.


Если не задавать, тогда ширина каждого столбца будет вычисляться в зависимости от содержимого.


Исходя из логики, можно понять, что в этом случае браузеру нужно два прохода. На первом он просто отображает все в таблице, подсчитывает ширину столбцов (мин, макс). На втором подстраивает ширину столбцов в зависимости от ширины таблицы.


Со временем вам скажут что таблица выглядит некрасиво, т.к. один из столбцов слишком широкий и


вот в этом столбце нам надо показать больше текста чем в этом, а у нас наоборот

И самая распространенная "фича":


  • это сокращение текста в ячейке с помощью ...

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


Первое разочарование, что если не задавать ширину столбцов, то сокращение не работает. В этом есть своя логика, т.к. на первом проходе браузер высчитывает мин/макс ширину колонки без сокращения, а тут мы пытаемся сократить текст. Необходимо либо все пересчитать повторно, либо игнорировать сокращение.


Сокращение реализуется просто, необходимо указать CSS свойства для ячейки:


CSS
td {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

И соответственно задать ширину колонки. По этой ссылке можно увидеть, что все настроено, но сокращение не работает.


В спецификации есть заметка, немного объясняющая, почему сокращение не работает:


If column widths prove to be too narrow for the contents of a particular table cell, user agents may choose to reflow the table

Опять же сужаться таблица будет до минимальной ширины содержимого. Но если применить свойство table-layout: fixed то таблица начнёт "слушаться" и сокращение заработает. Но автоподстройка ширины столбцов уже не работает.


Задание прокрутки таблицы


Вышеприведенный пример будет работать со скроллом и пользоваться этим можно. Однако возникает следующее требование:


здесь нам надо сделать, чтобы шапка таблицы оставалась на месте, а тело прокручивалось

Вторая дилемма с которой сталкиваются фронт-энд разработчики:


  • задание прокрутки/скролла в таблице

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


User agents may exploit the head/body/foot division to support scrolling of body sections independently of the head and foot sections. When long tables are printed, the head and foot information may be repeated on each page that contains table data

А есть и указание о том, что тело таблицы можно скроллить, а шапка и подвал будут оставаться на месте:


Table rows may be grouped into a table head, table foot, and one or more table body sections, using the THEAD, TFOOT and TBODY elements, respectively. This division enables user agents to support scrolling of table bodies independently of the table head and foot

А по факту браузеры этого не делают и скролл для таблицы необходимо придумывать/настраивать вручную.


Есть много способов это сделать, но все они сводяться к тому, что:


  1. мы не создаем дополнительную разметку и пытаемся прикрутить скролл к тому что есть (к телу таблицы, или оборачиваем в контейнер, а значение ячеек в шапке делаем абсолютно позиционированным)

Можно задать ограниченную высоту телу таблицы. Следующий пример показывает, что можно попробовать задать высоту тела таблицы.
В результате мы ломаем табличное отображение тела таблицы CSS свойством display: block, и при этом необходимо синхронизировать прокрутку шапки с телом таблицы.


  1. мы создаём дополнительную разметку (составные таблицы) и тогда при прокрутке оригинала мы синхронизируем дополнительную разметку

Этот вариант, где все предлагают/строят решения.


Примеры составных таблиц


Если нам необходима прокрутка тела таблицы, то без составных разметок не обойтись. Все примеры составных таблиц используют свои пользовательские разметки.


Одна из самых известных таблиц Data Tables использует следующую разметку:


Data Tables HTML
<div class="dataTables_scroll">
  <div class="dataTables_scrollHead">
    <div class="dataTables_scrollHeadInner">
      <table>
        <thead>
          <tr>
            <th></th>
            <th></th>
            <th></th>
          </tr>
        </thead>
      </table>
    </div>
  </div>
  <div class="dataTables_scrollBody">
    <table>
      <thead>
        <tr>
          <th><div class="dataTables_sizing"></div></th>
          <th><div class="dataTables_sizing"></div></th>
          <th><div class="dataTables_sizing"></div></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

Я намеренно сокращаю разметку, чтобы можно было составить общую картину, как выглядит разметка.


Мы видим в разметке две таблицы, хотя для пользователя это "видится" как одна.
Следующий пример React Bootstrap Table, если посмотреть в разметку, использует тоже две таблицы:


React Bootstrap Table HTML
<div class="react-bs-table-container">
  <div class="react-bs-table">
    <div class="react-bs-container-header table-header-wrapper">
      <table class="table table-hover table-bordered">
        <colgroup><col class=""><col class=""><col class=""></colgroup>
        <thead>
          <tr>
            <th></th>
            <th></th>
            <th></th>
          </tr>
        </thead>
      </table>
    </div>
    <div class="react-bs-container-body">
      <table class="table table-bordered">
        <colgroup><col class=""><col class=""><col class=""></colgroup>
        <tbody>
          <tr class="">
            <td></td>
            <td></td>
            <td></td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

Верхняя таблица отображает шапку, нижняя — тело. Хотя для пользователя кажется как будто бы это одна таблица.


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


А как же так получается, что тело таблицы (одна таблица) и шапка (другая таблица) подстраиваются под ширину контейнера и они никак не разъезжаются по ширине и совпадают друг с другом?


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


_adjustHeaderWidth
componentDidUpdate() {
  ...
  this._adjustHeaderWidth();
  ...
}

_adjustHeaderWidth() {
    ...
    // берем ширину столбцов из тела таблицы если есть хоть один ряд, или берем ширину <col> из тела таблицы
    // и присваиваем шапке полученные размеры
  }

Возникает вполне логичный вопрос, а зачем тогда вообще использовать тег <table>, если используется только автоподстройка ширины из стандартной таблицы?


И тут мы окажемся не первыми, некоторые вообще не используют табличную разметку. Например Fixed Data Table или React Table.


Разметка в примерах примерно такая:


Разметка
<div class="table">
  <div class="header">
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>
    </div>
  </div>
  <div class="body">
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>        
    </div>
  </div>
</div>

Отсюда название fixed table, т.е. для такой разметки мы должны заранее указать ширину всех столбцов (ширину таблицы, иногда и высоту строки). Хотя если мы хотим сокращение текста, все равно необходимо задавать ширину столбцов, даже в обычной таблице.


Следующая таблица Reactabular использует интересный подход в синхронизации.


Автор пошел дальше и сделал прокручиваемым не только тело, но и шапку таблицы. В браузерах которые показывают ползунок скролла — выглядит ужасно, зато в touch браузерах очень классно и функционально.


Если мы скроллим тело таблицы, то происходит синхронизация шапки, а если мы скроллим шапку, то происходит синхронизация тела.


А как же сделать автоподстройку ширины колонки в составной таблице спросите вы? Вот интересный способ использовать дополнительный проход браузера. Например в этой таблице ag Grid можно автоматически рассчитать подходящую ширину столбца.


В коде есть функция автоподстройки ширины колонки:


getPreferredWidthForColumn
public getPreferredWidthForColumn(column: Column): number {
  // создать <span style="position: fixed;">
  // добавить в него все ячейки столбца
  // вычислить ширину span (вычисляет браузер)
  // удаляем <span style="position: fixed;">
}

Реализация собственной таблицы


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


Все составные таблицы (и моя) страдают недостатком, у них нет стандарта как их кастомизировать/настраивать (и это логично, т.к. при реализации отказались от HTML4 таблицы).


Когда ты начинаешь изучать одну составную таблицу, потом начинаешь тратить время на её кастомизацию.


Затем для другого проекта изучаешь другую таблицу (например при переходе с Angular1 на React, или с jQuery на Vue), а кастомизация совсем другая.


Возникает логичный вопрос, а стоит ли потраченное время того? Стоит ли учить снова и снова связку фреймворк-таблица?


Может легче освоить для себя базовые моменты составной таблицы и тогда вы сможете делать свою таблицу на любом фреймворке (Angular/React/Vue/будущее...)? Например, на свою таблицу вы будете тратить 2 дня на старт, потом в течении 30 мин кастомизировать.


А можно подключить готовую фреймворк-таблицу за 30 мин и потом кастомизировать каждую фичу за 1 день.


К премеру, я покажу как сделать свою составную таблицу на React.


Таблица будет:


  • составной, синхронизировать шапку в зависимости от тела таблицы
  • подстраивать свою ширину если она меньше ширины контейнера

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


Разметка


Для разметки будем использовать div элементы. Если использовать display: inline-block для ячеек, тогда будет следующая разметка:


Inline block HTML
<div class="row">
  <div class="cell" style="width: 40px; display: inline-block;"></div>
  <div class="cell" style="width: 40px; display: inline-block;"></div>
</div>

Но есть одна проблема — браузер (не все браузеры) интерпретирует пустые места между ячейками как текстовые ноды.


Есть отличная статья, как с этим бороться.


И если мы используем шаблонизатор (EJS, JSX, Angular, Vue), то это легко решить:


inline block fixed HTML
<div class="row">
  <div class="cell" style="width: 40px;">{value}</div><div class="cell" style="width: 40px;">{value}</div>
</div>

Однако уже 2017 год, flexbox давно поддерживается, я делал на нем проекты еще в 2014 для IE11.


А сегодня можно вообще не стесняться его. Это упростит нам задачу, можно будет делать столько пустых нод, сколько нужно:


Flexbox HTML
<div class="row" style="display: flex; flex-direction: row;">
  <div class="cell" style="width: 40px; flex: 0 0 auto;">{value}</div>
  <!-- дальше пустое место -->

  <div class="cell" style="width: 40px; flex: 0 0 auto;">{value}</div>
</div>

Общие моменты использования


Таблица должна встраиваться в Redux архитектуру, примеры таких таблиц предалагают подключать свои reducers.


Мне этот подход не нравится. По моему мнению, разработчик должен контроллировать процесс сортировки, фильтрации. Это требует дополнительного кода.


Вместо такого "черного ящика", который потом сложно кастомизировать:


Обычное подключение
render() {
  return (
    <div>
      <Table filter={...} data={...} columns={...} format={...} etc={...} />
    </div>
  )
}

разработчик должен будет писать:


Собственное подключение
render() {
  const 
    descriptions = getColumnDescriptions(this.getTableColumns()),
    filteredData = filterBy([], []),
    sortedData = sortBy(filteredData, []);
  return (
    <div>
      <TableHeader descriptions={descriptions} />
      <TableBody data={sortedData} descriptions={descriptions} keyField={"Id"} />
    </div>
  )
}

Разработчик должен сам прописывать шаги: вычислить описание колонок, отфильтровать, отсортировать.


Все функции/конструкторы getColumnDescriptions, filterBy, sortBy, TableHeader, TableBody, TableColumn будут импортироваться из моей таблицы.


В качестве данных будет использоваться массив объектов:


Пример данных
[
  { "Company": "Alfreds Futterkiste", "Cost": "0.25632" },
  { "Company": "Francisco Chang", "Cost": "44.5347645745" },
  { "Company": "Ernst Handel", "Cost": "100.0" },
  { "Company": "Roland Mendel", "Cost": "0.456676" },
  { "Company": "Island Trading Island Trading Island Trading Island Trading Island Trading", "Cost": "0.5" },
]

Мне понравился подход создания описания колонок в jsx в качестве элементов.


Будем использовать ту же идею, однако, чтобы сделать независимыми шапку и тело таблицы, будем вычислять описание один раз и передавать его и в шапку и в тело:


Описание колонок и подключение
getTableColumns() {
  return [
    <TableColumn row={0} width={["Company", "Cost"]}>first header row</TableColumn>,
    <TableColumn row={1} dataField={"Company"} width={200}>
      Company
    </TableColumn>,
    <TableColumn row={1} dataField={"Cost"} width={100}>
      Cost
    </TableColumn>,
  ];
}

render() {
  const 
    descriptions = getColumnDescriptions(this.getTableColumns());
  return (
    <div>
      <TableHeader descriptions={descriptions} />
      <TableBody data={[]} descriptions={descriptions} keyField={"Id"} />
    </div>
  )
}

В функции getTableColumns мы создаем описание колонок.


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


Обязательно указываем row — число, которое показывает индекс строки в шапке (если шапка будет группироваться).


Параметр dataField, определяет какой ключ из объекта использовать для получения значения.


Ширина width тоже обязательный параметр, может задаватся как числом или как массивом ключей от которых зависит ширина.


В примере верхняя строка в таблице row={0} зависит от ширины двух колонок ["Company", "Cost"].


Элемент TableColumn "фейковый", он никогда не будет отображаться, а вот его содержимое this.props.children — отображается в ячейке шапки.


Разработка


На основе описаний колонок сделаем функцию, которая будет разбивать описания по рядам и по ключам, а также будет сортировать описания по рядам в результирующем массиве:


getColumnDescriptions
function getColumnDescriptions(children) {
  let byRows = {}, byDataField = {};
  React.Children.forEach(children, (column) => {
    const {row, hidden, dataField} = column.props;
    if (column === null || column === undefined || typeof row !== 'number' || hidden) { return; }
    if (!byRows[row]) { byRows[row] = [] }
    byRows[row].push(column);
    if (dataField) { byDataField[dataField] = column }
  });
  let descriptions = Object.keys(byRows).sort().map(row => {
    byRows[row].key = row;
    return byRows[row];
  });
  descriptions.byRows = byRows;
  descriptions.byDataField = byDataField;
  return descriptions;
}

Теперь обработанные описания передаём в шапку и в тело для отображения ячеек. Шапка будет строить ячейки так:


Header render
getFloor(width, factor) {
  return Math.floor(width * factor);
}

renderChildren(descriptions) {
  const {widthFactor} = this.props;
  return descriptions.map(rowDescription => {
    return <div className={styles.tableHeaderRow} key={rowDescription.key}>
      {rowDescription.map((cellDescription, index) => {
        const {props} = cellDescription;
        const {width, dataField} = props;
        const _width = Array.isArray(width) ?
          width.reduce((total, next) => {
            total += this.getFloor(descriptions.byDataField[next].props.width, widthFactor);
            return total;
          }, 0) :
          this.getFloor(width, widthFactor);
        return <div className={styles.tableHeaderCell}
                    key={dataField || index}
                    style={{ width: _width + 'px' }}>
          {cellDescription.props.children}
        </div>
      })}
    </div>
  })
}

render() {
  const {className, descriptions} = this.props;

  return (
    <div className={styles.tableHeader} ref={this.handleRef}>
      {this.renderChildren(descriptions)}
    </div>
  )
}

Тело таблицы будет строить ячейки тоже на основе обработанных описаний колонок:


Body render
renderDivRows(cellDescriptions, data, keyField) {
  const {rowClassName, widthFactor} = this.props;
  return data.map((row, index) => {
    return <div className={`${styles.tableBodyRow} ${rowClassName}`} key={row[keyField]}
                data-index={index} onClick={this.handleRowClick}>
      {cellDescriptions.map(cellDescription => {
        const {props} = cellDescription;
        const {dataField, dataFormat, cellClassName, width} = props;
        const value = row[dataField];
        const resultValue = dataFormat ? dataFormat(value, row) : value;
        return <div className={`${styles.tableBodyCell} ${cellClassName}`} key={dataField}
                    data-index={index} data-key={dataField} onClick={this.handleCellClick}
                    style={{ width: this.getFloor(width, widthFactor) + 'px' }}>
          {resultValue ? resultValue : '\u00A0'}
        </div>
      })}
    </div>
  });
}

getCellDescriptions(descriptions) {
  let cellDescriptions = [];
  descriptions.forEach(rowDescription => {
    rowDescription.forEach((cellDescription) => {
      if (cellDescription.props.dataField) {
        cellDescriptions.push(cellDescription);
      }
    })
  });
  return cellDescriptions;
}

render() {
  const {className, descriptions, data, keyField} = this.props;
  const cellDescriptions = this.getCellDescriptions(descriptions);

  return (
    <div className={`${styles.tableBody} ${className}`} ref={this.handleRef}>
      {this.renderDivRows(cellDescriptions, data, keyField)}
    </div>
  )
}

Тело таблицы использует описания у которых есть свойство dataField, поэтому описания фильтруются используя функцию getCellDescriptions.


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


Слушатели
componentDidMount() {
  this.adjustBody();
  window.addEventListener('resize', this.adjustBody);
  if (this.tb) {
    this.tb.addEventListener('scroll', this.adjustScroll);
  }
}

componentWillUnmount() {
  window.removeEventListener('resize', this.adjustBody);
  if (this.tb) {
    this.tb.removeEventListener('scroll', this.adjustScroll);
  }
}

Подстройка ширины таблицы происходит следующим образом.


После отображения берётся ширина контейнера, сравнивается с шириной всех ячеек, если ширина контейнера больше, увеличивается ширина всех ячеек.


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


Следующие функции реализованы в таблице, однако разработчик может использовать свои. Чтобы использовать уже реализованные, необходимо их импортировать и прилинковать к текущему компоненту:


Линковка
constructor(props, context) {
  super(props, context);

  this.state = {
    activeSorts: [],
    activeFilters: [],
    columnsWidth: {
      Company: 300, Cost: 300
    },
    widthFactor: 1
  };

  this.handleFiltersChange = handleFiltersChange.bind(this);
  this.handleSortsChange = handleSortsChange.bind(this);
  this.handleAdjustBody = handleAdjustBody.bind(this);
  this.getHeaderRef = getHeaderRef.bind(this, 'th');
  this.getBodyRef = getBodyRef.bind(this, 'tb');
  this.syncHeaderScroll = syncScroll.bind(this, 'th');
}

Функция подстройки ширины:


adjustBody
adjustBody() {
  const {descriptions, handleAdjustBody} = this.props;
  if (handleAdjustBody) {
    const cellDescriptions = this.getCellDescriptions(descriptions);
    let initialCellsWidth = 0;
    cellDescriptions.forEach(cd => {
      initialCellsWidth += cd.props.width;
    });
    handleAdjustBody(this.tb.offsetWidth, initialCellsWidth);
  }
}

Функция синхронизация шапки:


adjustScroll
adjustScroll(e) {
  const {handleAdjustScroll} = this.props;
  if (typeof handleAdjustScroll === 'function') {
    handleAdjustScroll(e);
  }
}

Ключевая особенность таблицы для redux — это то, что она не имеет своего внутреннего состояния (она должна иметь состояние, но только в том месте, где укажет разработчик).


И подстройка ширины adjustBody и синхронизация скролла adjustScroll — это функции которые изменяют состояние у прилинкованного компонента.


Внутрь TableColumn можно вставлять любую jsx разметку. Зачастую используются такие варианты: текст, кнопка сортировки и кнопка фильтрации.


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


Состояние
this.state = {
  activeSorts: [],
  activeFilters: [],
};

Передаем в таблицу массив активных сортировок/фильтраций:


getTableColumns
getTableColumns() {
  const {activeFilters, activeSorts, columnsWidth} = this.state;
  return [
    <TableColumn row={0} width={["Company", "Cost"]}>first header row</TableColumn>,
    <TableColumn row={1} dataField={"Company"} width={300}>
      <MultiselectDropdown title="Company" activeFilters={activeFilters} dataField={"Company"}
                           items={[]} onFiltersChange={this.handleFiltersChange} />
    </TableColumn>,
    <TableColumn row={1} dataField={"Cost"} width={300}>
      <SortButton title="Cost" activeSorts={activeSorts} dataField={"Cost"}
                  onSortsChange={this.handleSortsChange} />
    </TableColumn>,
  ];
}

Компонент сортировки SortButton и компонент фильтрации MultiselectDropdown при изменении "выбрасывают" новые активные фильтры/сортировки, которые разработчик должен заменить в состоянии. Массивы activeSorts и activeFilters как раз и предполагают, что возможна множественная сортировка и множественная фильтрация по каждой колонке.


К сожалению, формат статьи не позволяет описать всех тонкостей, поэтому предлагаю сразу посмотреть результат.


Итого разработчику в таблице необходимо:


  • автоподстройка ширины таблицы под ширину контейнера
  • прокрутка тела таблицы и синхронизация шапки
  • сортировка таблицы (возможна множественная сортировка)
  • фильтрация таблицы (возможна множественная фильтрация)

Все это я реализовал в примере. Надеюсь теперь, при переходе на новый фреймворк, у вас как минимум появился выбор — брать готовую или сделать свою таблицу.


Исходники находятся здесь.

Поделиться с друзьями
-->

Комментарии (26)


  1. vintage
    26.06.2017 23:23

    Можно сделать проще: с помощью transform смещать ячейки на величину равную смещению скроллинга. Таким образом можно зафиксировать и верхнюю шапку и боковую.
    http://mol.js.org/#demo=mol_grid


    1. volodalexey
      27.06.2017 08:41

      Чем проще то? В Вашем примере надо синхронизировать transform свойство, а в большинстве случаев синхронизируют scroll(Top/Left) свойство. Это в динамике — а вот в статике в примере разметка самая вроде "стандартная". Т.е. чем полезен пример — это возможно печать.


      1. vintage
        27.06.2017 08:50

        в большинстве случаев синхронизируют scroll(Top/Left) свойство

        … и размеры строк/колонок.


        1. volodalexey
          27.06.2017 08:57

          Попробовал ваш пример "на печать" — можно смело присваивать ярлык еще одна кастомная неудача, только еще и с transform.


          1. vintage
            27.06.2017 10:19


            Что не так?


            1. volodalexey
              27.06.2017 10:36

              Если проскроллить и потом отправить на печать — то ломается.


              1. vintage
                27.06.2017 13:08

                Очевидно при печати виртуальный скролл нужно отключать.


            1. justboris
              27.06.2017 10:37

              Так в таблице же больше строк, чем у вас поместилось на страницу.


              В моем комментарии я имел в виду поддержку печати, что когда отправить страницу на печать, и там печатаются все данные, а не только видные в текущем скролле.


              Соответственно, ваша реализация тоже так не может, как и любая другая, с кастомным скроллом.


  1. daggert
    27.06.2017 01:38
    +3

    Не надо ради любого бога делать таблицу «свою», пользуйте стандартный функционал везде где возможно. При копировании таблицы в текстовый файл, что часто нужно для банальной отчетности, вся ваша кастомная таблица превращается в кашу из строк, которую приходится возвращать в нормальный вид руками.


    1. justboris
      27.06.2017 02:29
      +1

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


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


      1. vintage
        27.06.2017 06:42

        Либо и то и другое, как я описал выше :-)


      1. daggert
        27.06.2017 11:49

        Внезапно — шапку таблицы можно прибить вынесением ее в отдельную fixed-таблицу (: Тогда при копировании ее общие грани сливаются в одну.


        1. justboris
          27.06.2017 11:56

          Если вы повнимательнее почитаете статью, то заметите, что там так и сделано


          1. daggert
            27.06.2017 12:23

            Немного не раскрыл свой комментарий, прошу прощения: Самое простое прибивание можно сделать без обертываний дополнительных, только две таблицы + CSS.


            1. justboris
              27.06.2017 12:54

              При скролле вправо-влево шапка останется на месте, и подписи к колонкам будут показывать не на те данные. Именно это и решается в статье с помощью JS.


              Кстати, а почему вы говорите, что у вас копирование не работает? Вы пробовали копировать текст из примера в статье? У меня весь текст копируется нормально.


              1. daggert
                27.06.2017 13:09

                Да, вы правы, скролл влево/вправо я пропустил, акцентировав свое внимание на вертикальной прокрутке.

                У меня копирование работает, однако при вставке в ворд — все блоки выкладываются в список, а не в таблицу (https://volodalexey.github.io/front-end-notes/ вот этот пример?)


                1. justboris
                  27.06.2017 15:34

                  Так это из-за того, что в контенте таблицы были символы перевода строки "\n". Кастомная реализация тут не причем, так накосячить можно и с обычной таблицей


                  1. daggert
                    27.06.2017 15:44

                    Не совсем. При копировании нативной таблицы в ворд/райтер — вставится как таблица, благо что редактор теперь это позволяет, с эмуляцией таблицы такое не пройдет, ибо при копировании в буфер обмена не уходят стили. Если вы сделаете блоки без переносов — будет каша текста в одну строку. Если будете ставить пробелы — будет разделенная пробелами строка, но таблицу вы из div и span не скопируете никак.


                    1. justboris
                      27.06.2017 15:48

                      Я, кажется, вас понял.


                      Нужно, чтобы в разметке были только теги table и ничего лишнего. Тогда Word или другой аналогичный редактор распарсит контент и вставит его в правильном формате.


                      Попробую учесть этот сценарий в следующий раз, как буду делать таблицы.


                      1. daggert
                        27.06.2017 23:46

                        Да (: Извините что не внятно выразил свои мысль изначально.


    1. volodalexey
      27.06.2017 08:47

      В статье был посыл в том, что, чем брать кастомную другую и килотонны JS кода, лучше сделать кастомную свою. Конечно стандартная разметка для того и сделана, чтобы быть стандартом. Но Вы уже точно опоздали, этих кастомных таблиц — тьма. Впечатление производит "динамика" — такова тенденция.


      1. kursoriks
        27.06.2017 10:37

        этих кастомных таблиц — тьма.

        В то-то и дело, из-за чего для того чтобы из всего этого выбрать оптимальный вариант, проще сесть и написать свой с блекджеком и маффинами. Да и к слову, никто не запрещает повесить эвент на scroll и использовать transform правильно и по назначению, как-никак за окном 2017 год.


  1. clemencov
    27.06.2017 13:15
    +3

    Еще position: sticky можно использовать: https://codepen.io/clemencov/pen/mwqwpd, поддержка: http://caniuse.com/#search=sticky.

    Хотя спецификация не предусматривает работу с табличными элементами, в Хроме 56+ работает.


    1. volodalexey
      27.06.2017 13:41

      Я когда то хотел просто с шапкой так на сайте сделать. К сожалению в то время понял, что поддержки браузеров нет, что-то не могут видимо договориться. Сейчас смотрю ситуация улучшается.


  1. jankovsky
    27.06.2017 15:46

    Просто диву даешься. Верстка с , , и прочим уже давно канула в лету. Сегодня уже стандарт де-факто «верстка плавающими блоками» как минимум. Div-ы товарищи! Div-ы!


    1. freetitelu
      27.06.2017 18:46

      Ну да, зря они там в своих спецификациях придумали куча ненужных сущностей. input, button, table, ul, a (сотни их) и прочая семантика — ни к чему (где-то в мире при таких словах плачет В.Макеев). Div-ы товарищи! Div-ы!