Таблицы! Таблицы? Таблицы… |
В статье я покажу стандартную табличную разметку, какие у неё есть альтернативы. Дам пример собственной таблицы и разметки, а также опишу общие моменты её реализации.
Когда появилась необходимость в HTML разметке показывать таблицы — изобрели тег
Header 1 | Header 2 |
---|---|
1.1 | 1.2 |
2.1 | 2.2 |
Однако можно использовать "каноничную" разметку:
Header 1
Header 2
1.1
1.2
2.1
2.2
Если нужна таблица без шапки и в то же время нам необходимо контроллировать ширину столбцов:
1.1
1.2
2.1
2.2
Чаще всего нам в разметке необходимо получить следующее. У нас есть некий контейнер с заданной шириной или с заданной максимальной шириной. Внутри него мы хотим вписать таблицу.
Если ширина таблицы больше чем контейнер, тогда необходимо показывать скролл для контейнера. Если ширина таблицы меньше чем контейнер, тогда необходимо расширять таблицу до ширины контейнера.
Но ни в коем случае мы не хотим, чтобы таблица сделала наш контейнер шире чем мы задали.
По этой ссылке можно уведеть контейнер с таблицей в действии. Если мы будем сужать контейнер, то в тот момент, когда таблица уже больше не сможет сужаться — появиться скролл.
Первая дилемма с которой сталкиваются фронт-энд разработчики — это задавать или не задавать ширину столбцов.
Если не задавать, тогда ширина каждого столбца будет вычисляться в зависимости от содержимого.
Исходя из логики, можно понять, что в этом случае браузеру нужно два прохода. На первом он просто отображает все в таблице, подсчитывает ширину столбцов (мин, макс). На втором подстраивает ширину столбцов в зависимости от ширины таблицы.
Со временем вам скажут что таблица выглядит некрасиво, т.к. один из столбцов слишком широкий и
вот в этом столбце нам надо показать больше текста чем в этом, а у нас наоборот
И самая распространенная "фича":
...
Т.е. если текст в ячейке вылазит за ширину колонки, то его необходимо сокращать и в конце добавлять ...
.
Первое разочарование, что если не задавать ширину столбцов, то сокращение не работает. В этом есть своя логика, т.к. на первом проходе браузер высчитывает мин/макс ширину колонки без сокращения, а тут мы пытаемся сократить текст. Необходимо либо все пересчитать повторно, либо игнорировать сокращение.
Сокращение реализуется просто, необходимо указать 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
А по факту браузеры этого не делают и скролл для таблицы необходимо придумывать/настраивать вручную.
Есть много способов это сделать, но все они сводяться к тому, что:
Можно задать ограниченную высоту телу таблицы. Следующий пример показывает, что можно попробовать задать высоту тела таблицы.
В результате мы ломаем табличное отображение тела таблицы CSS свойством display: block
, и при этом необходимо синхронизировать прокрутку шапки с телом таблицы.
Этот вариант, где все предлагают/строят решения.
Если нам необходима прокрутка тела таблицы, то без составных разметок не обойтись. Все примеры составных таблиц используют свои пользовательские разметки.
Одна из самых известных таблиц Data Tables использует следующую разметку:
Я намеренно сокращаю разметку, чтобы можно было составить общую картину, как выглядит разметка.
Мы видим в разметке две таблицы, хотя для пользователя это "видится" как одна.
Следующий пример React Bootstrap Table, если посмотреть в разметку, использует тоже две таблицы:
Верхняя таблица отображает шапку, нижняя — тело. Хотя для пользователя кажется как будто бы это одна таблица.
Опять же пример использует синхронизацию прокрутки, если прокрутить тело таблицы, то произойдет синхронизация шапки.
А как же так получается, что тело таблицы (одна таблица) и шапка (другая таблица) подстраиваются под ширину контейнера и они никак не разъезжаются по ширине и совпадают друг с другом?
Тут кто как умеет так и синхронизирует, например, вот функция синхронизации ширины из вышеприведенной библиотеки:
componentDidUpdate() {
...
this._adjustHeaderWidth();
...
}
_adjustHeaderWidth() {
...
// берем ширину столбцов из тела таблицы если есть хоть один ряд, или берем ширину из тела таблицы
// и присваиваем шапке полученные размеры
}
Возникает вполне логичный вопрос, а зачем тогда вообще использовать тег
разработчик должен будет писать:
render() {
const
descriptions = getColumnDescriptions(this.getTableColumns()),
filteredData = filterBy([], []),
sortedData = sortBy(filteredData, []);
return (
)
}
Разработчик должен сам прописывать шаги: вычислить описание колонок, отфильтровать, отсортировать.
Все функции/конструкторы 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 [
first header row ,
Company
,
Cost
,
];
}
render() {
const
descriptions = getColumnDescriptions(this.getTableColumns());
return (
)
}
В функции getTableColumns
мы создаем описание колонок.
Все обязательные свойства я могу описать через propTypes
, но после того как их вынесли в отдельную библиотеку — это решение кажется сомнительным.
Обязательно указываем row
— число, которое показывает индекс строки в шапке (если шапка будет группироваться).
Параметр dataField
, определяет какой ключ из объекта использовать для получения значения.
Ширина width
тоже обязательный параметр, может задаватся как числом или как массивом ключей от которых зависит ширина.
В примере верхняя строка в таблице row={0}
зависит от ширины двух колонок ["Company", "Cost"]
.
Элемент TableColumn
"фейковый", он никогда не будет отображаться, а вот его содержимое this.props.children
— отображается в ячейке шапки.
На основе описаний колонок сделаем функцию, которая будет разбивать описания по рядам и по ключам, а также будет сортировать описания по рядам в результирующем массиве:
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;
}
Теперь обработанные описания передаём в шапку и в тело для отображения ячеек. Шапка будет строить ячейки так:
getFloor(width, factor) {
return Math.floor(width * factor);
}
renderChildren(descriptions) {
const {widthFactor} = this.props;
return descriptions.map(rowDescription => {
return
{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
{cellDescription.props.children}
})}
})
}
render() {
const {className, descriptions} = this.props;
return (
{this.renderChildren(descriptions)}
)
}
Тело таблицы будет строить ячейки тоже на основе обработанных описаний колонок:
renderDivRows(cellDescriptions, data, keyField) {
const {rowClassName, widthFactor} = this.props;
return data.map((row, index) => {
return
{cellDescriptions.map(cellDescription => {
const {props} = cellDescription;
const {dataField, dataFormat, cellClassName, width} = props;
const value = row[dataField];
const resultValue = dataFormat ? dataFormat(value, row) : value;
return
{resultValue ? resultValue : '\u00A0'}
})}
});
}
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 (
{this.renderDivRows(cellDescriptions, data, keyField)}
)
}
Тело таблицы использует описания у которых есть свойство 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() {
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(e) {
const {handleAdjustScroll} = this.props;
if (typeof handleAdjustScroll === 'function') {
handleAdjustScroll(e);
}
}
Ключевая особенность таблицы для redux
— это то, что она не имеет своего внутреннего состояния (она должна иметь состояние, но только в том месте, где укажет разработчик).
И подстройка ширины adjustBody
и синхронизация скролла adjustScroll
— это функции которые изменяют состояние у прилинкованного компонента.
Внутрь TableColumn
можно вставлять любую jsx разметку. Зачастую используются такие варианты: текст, кнопка сортировки и кнопка фильтрации.
Для массива активных сортировок/фильтраций разработчик должен создать состояние и передавать его в таблицу.
this.state = {
activeSorts: [],
activeFilters: [],
};
Передаем в таблицу массив активных сортировок/фильтраций:
getTableColumns() {
const {activeFilters, activeSorts, columnsWidth} = this.state;
return [
first header row ,
,
,
];
}
Компонент сортировки SortButton
и компонент фильтрации MultiselectDropdown
при изменении "выбрасывают" новые активные фильтры/сортировки, которые разработчик должен заменить в состоянии. Массивы activeSorts
и activeFilters
как раз и предполагают, что возможна множественная сортировка и множественная фильтрация по каждой колонке.
К сожалению, формат статьи не позволяет описать всех тонкостей, поэтому предлагаю сразу посмотреть результат.
Итого разработчику в таблице необходимо:
Все это я реализовал в примере. Надеюсь теперь, при переходе на новый фреймворк, у вас как минимум появился выбор — брать готовую или сделать свою таблицу.
Исходники находятся здесь.
Комментировать | « Пред. запись — К дневнику — След. запись » | Страницы: [1] [Новые] |