https://habrahabr.ru/post/350292/Еще одна небольшая статейка попроще вдогонку. Расскажу, как я рисую таблицы во
Vue.
Компонентов-таблиц для Vue наделано немало. С различными возможностями. И везде по-разному таблица собирается в
template страницы или какого-то компонента.
В основном происходит это как-то так:
<template>
<cmp-table :items="items" :columns="columns"/>
</template>
<script>
export default {
name: 'page',
data() {
return {
items: [
{ id: 1, name: 'Sony' } ,
{ id: 2, name: 'Apple' },
{ id: 3, name: 'Samsung' } ],
columns: [
{ prop: 'id', title: 'ID' },
{ prop: 'name', title: 'Name' } ]
}
}
}
</script>
Тут мы передаем в компонент
cmp-table данные (
items) и настройки колонок (
columns). А сам компонент уже рендерит таблицу по этим настройкам.
Настройки бывают организованны по всякому. Просто отдельно настройки колонок или вообще все в кучу — настройки колонок, таблицы, каких-то действий и т.п.
Мне в таком подходе не нравится то, как организована настройка рендера колонок. Как их названий в шапке
thead таблицы, так и самого содержимого колонок.
Этот функционал хочется видеть в самом
template, там строить колонки (их содержимое и шапку). Так нагляднее и удобнее. Как по мне.
Такой подход, например, используется в самом популярном вроде на сегодняшний день сборнике компонентов —
Element.
Там это выглядит так:
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="Date" width="180"/>
<el-table-column prop="name" label="Name" width="180"/>
<el-table-column prop="address" label="Address"/>
</el-table>
</template>
<script>
export default {
name: 'page',
data() {
return {
tableData: [{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
}, {
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
}, {
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
}]
}
}
}
</script>
Здесь все наглядно. Мы сразу представляем себе общий вид колонок и ячеек. А в компонент
el-table передаем лишь данные и настройки самой таблицы.
И все бы ничего. Нравится мне в общем
Element. Но часто замечал, что их таблицы подтормаживают. Полез в их код. И офигел от того, как заморочисто там происходит рендер строк, ячеек и всего остального. Просто тонны кода. И стал я думать, как можно сделать построение таблицы так-же как у них, только проще. Намного проще.
Сейчас расскажу и покажу, что у меня получилось.
Сразу о том, что в итоге будет представлять собой наш компонент. Чтобы происходящее дальше проще понималось:
- построение колонок аналогично Element (el-table)
- возможность кастомизации вида ячеек
- очень мало кода
- и возможность расширения функционала без особых проблем
Состоять наша таблица будет из двух частей:
- компонента TableColumn — с его помощью мы будем формировать вид шапки таблицы и ячеек
- компонента Table — в нем будет все собираться и рендериться
Компонент TableColumn — или краткость наше все
table-column.js:
export default {
name: 'vu-table-column',
props: ['prop', 'title']
};
Это компонент
Vue c именем
vu-table-column и парой входящих параметров:
- prop — в него передаем наименование свойства строки переданных данных в таблицу.
- title — название этого свойства, которое будет отображаться в шапке таблицы.
Да — это все, что нам нужно, для реализации текущих требований к нашему компоненту. Дальше будет понятно, почему здесь все так просто.
Компонент Table — или зачем писать больше кода, если можно меньше
table.jsimport './style.scss'
import { get } from 'lodash'
export default {
name: 'vu-table',
props: {
rows: {
type: Array,
required: true
}
},
methods: {
renderColumns(h, row, columnsOptions) {
return columnsOptions.map((column, index) => {
return h('td', { class: 'vu-table__tbody-td' }, [
column.scopedSlot ? column.scopedSlot({row, items: this.rows}) : row[column.prop]
])
})
}
},
render(h) {
const columnsOptions = this.$slots.default.filter(item => {
return (item.componentOptions && item.componentOptions.tag === 'vu-table-column')
}).map(column => {
return Object.assign({}, column.componentOptions.propsData, {
scopedSlot: get(column, 'data.scopedSlots.default')
}
)
})
const columnsHead = columnsOptions.map((column, index) => {
return h('th', { class: 'vu-table__thead-th', key: index}, column.title)
})
const rows = this.rows.map((row, index) => {
return h('tr', { key: index }, [ this.renderColumns(h, row, columnsOptions) ])
})
return h('table', { class: 'vu-table' }, [
h('thead', { class: 'vu-table__thead' }, [
h('tr', [ columnsHead ])
]),
h('tbody', { class: 'vu-table__tbody' }, [ rows ])
])
}
};
И пройдемся по коду.
import './style.scss'
import { get } from 'lodash'
export default {
name: 'vu-table',
props: {
rows: {
type: Array,
required: true
}
}
...
}
В начале импортируем стили таблицы.
И еще я тут использую
get-функцию
lodash-а. Это не обязательно. Она здесь чтобы код в итоге был короче.
Дальше входящий параметр
rows, куда передаем наши данные, в виде массива строк.
Теперь по
render-функции:
render(h) {
const columnsOptions = this.$slots.default.filter(item => {
return (item.componentOptions && item.componentOptions.tag === 'vu-table-column')
}).map(column => {
return Object.assign({}, column.componentOptions.propsData, {
scopedSlot: get(column, 'data.scopedSlots.default')
}
)
})
const columnsHead = columnsOptions.map((column, index) => {
return h('th', { class: 'vu-table__thead-th', key: index}, column.title)
})
const rows = this.rows.map((row, index) => {
return h('tr', { key: index }, [ this.renderColumns(h, row, columnsOptions) ])
})
return h('table', { class: 'vu-table' }, [
h('thead', { class: 'vu-table__thead' }, [
h('tr', [ columnsHead ])
]),
h('tbody', { class: 'vu-table__tbody' }, [ rows ])
])
}
В
columnOptions мы формируем настройки наших колонок и ячеек.
Для этого сначала собираем, фильтруя, все элементы с тегом
vu-table-column ( компонент
TableColumn) из дефолтного слота (
this.$slots.default) компонента
Table.
Компонент
TableColumn нам нужен пока только для того, чтобы передать настройки колонки удобным и наглядным образом. Вот почему в
TableColumn нет
render-функции. Потому-что мы не рендерим этот компонент. Только забираем данные.
И пробегаемся по массиву отфильтрованных
vu-table-column, формируем массив объектов с входящими
props-ами из
vu-table-column и добавляем свойство
scopedSlot. В нем будет храниться дефолтный
слот с ограниченной областью видимости, если таковой передан в
vu-table-column в template страницы. Представляет он собой функцию, которая рендерит содержимое этого слота. И в нее можно передать любые параметры, которые используются в шаблоне этого слота. Этот слот мы и будем использовать для кастомного вида ячеек.
Дальше собираем
columnsHead (ячейки шапки таблицы) — пробегаемся по выше определенным настройкам колонок (
columnOptions) выдергивая
title — название колонки, которое мы передали в
vu-table-column.
Формируем массив
rows (собственно итоговых строк таблицы) — пробегаемся по нашим входящим
rows, и в каждом элементе
tr выводим ячейки с помощью метода
renderColumns:
renderColumns(h, row, columnsOptions) {
return columnsOptions.map((column, index) => {
return h('td', { class: 'vu-table__tbody-td' }, [
column.scopedSlot ?
column.scopedSlot({row, items: this.rows}) : row[column.prop]
])
})
}
В метод мы передаем
h-функцию (псевдоним функции
$createElement, которая рендерит
vNode), данные строки и массив настроек колонок
columnsOptions.
И собираем массив ячеек, в которых рендерим:
- либо кастомный вид, если в настройках колонки есть слот с ограниченной видимостью, запуская функцию scopedSlot с параметрами row (содержащим объект строки) и items (данные, переданные в vu-table). Здесь мы можем передать все, что нам надо
- либо просто значение свойства с именем column.prop из строки row
при рендере массивов-елементов, не забывайте каждому елементу присваивать параметр key. Иначе, при обновлении данных, могут возникнуть коллизии в отображаемых данных
И в конце
render-функции выводим итоговую таблицу, с вставленными в нее ячейками шапки и отрендеренными строками.
Все!
Вот и весь код, для рисования ячеек и шапки.
Дальше уже можно прилеплять фильтрацию, скрытие/показ колонок, сортировку и все, что душа пожелает. Это уже следующая история. И будет в следующей статье. Дабы не писать бесконечные тексты.
И пример использования написанного компонента:
example.vue<template lang="html">
<div>
<vu-table :rows="rows">
<vu-table-column prop="id" title="ID">
<template slot-scope="{ row }">
<b>{{ row.id }}</b>
</template>
</vu-table-column>
<vu-table-column prop="name" title="Name"/>
<vu-table-column prop="rating" title="Rating">
<template slot-scope="{ row, items }">
{{ row.rating }}
<b v-if="items.every(item => item.rating <= row.rating)">Best choiсe!</b>
</template>
</vu-table-column>
</vu-table>
</div>
</template>
<script>
export default {
name: 'example-page',
data() {
return {
rows: [
{ id: 1,
name: 'Sony',
rating: 777 },
{ id: 2,
name: 'Apple',
rating: 555 },
{ id: 3,
name: 'Samsung',
rating: 333 }
]
}
}
};
</script>
Тут мы используем как обычные
title, так и кастомный вид ячеек.
И в ячейках
Rating мы, используя данные
items (которые передали в функцию
scopedSlot и содержащие входящий массив со строками) и значение свойства
rating определяем, является ли текущая строка с наибольшим рейтингом. Если да, выводим жирным текстом 'Best choice!'
И готовый результат:

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