Пишем компонент — таблицу, не совсем обычным способом

  • Tutorial
Еще одна небольшая статейка попроще вдогонку. Расскажу, как я рисую таблицы во 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)
  • возможность кастомизации вида ячеек
  • очень мало кода
  • и возможность расширения функционала без особых проблем

Состоять наша таблица будет из двух частей:

  1. компонента TableColumn — с его помощью мы будем формировать вид шапки таблицы и ячеек
  2. компонента Table — в нем будет все собираться и рендериться

Компонент TableColumn — или краткость наше все


table-column.js:

export default {
  name: 'vu-table-column',
  props: ['prop', 'title']
};

Это компонент Vue c именем vu-table-column и парой входящих параметров:

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

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

Компонент Table — или зачем писать больше кода, если можно меньше


table.js
import './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!'

И готовый результат:



Вот такой компонент в итоге получился.

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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    0
    Sticky columns, material theme — и прям то что нужно!
      +1

      Может быть имеет смысл размещать ДЕМО на JSBIN?
      Чтобы можно было потом поиграться с кодом ??


      ---Нееее мы до сих пор живем в 90-ххх

        0
        Чем использую таблицу на элементе. Вы считаете, что у вас получится сделать более производительную таблицу, если накрутить весь то функционал, который есть в оригинальной таблице?
          0
          Как накручу соответствующий функционал, тогда и будет видно, производительней получится или нет, чем у таблицы Element. Для меня это своего рода вызов. И вообще надо стараться все упрощать. Так жить легче. Я упростил только лишь сам рендер ячеек. У Element этот рендер очень заморочистый и реализован в большом количестве кода. И я не вижу в этом особого смысла. Может им так удобнее. Не знаю, в чем причина.
          Если вы не видели исходники, посмотрите. А до уровня функциональности их таблицы я на днях доведу свой компонент, тогда и можно будет сравнить.
            0
            А до уровня функциональности их таблицы я на днях доведу

            Думаете это получится так быстро? У них там целая команда работает) А так да, видел их исходники конечно, даже добавлял функциональность(так же фиксил небольшие баги), которую мне не хватала, так же являюсь контрибутором данного проекта. Незнаю что тут можно придумать другого для производительности, но перерендер каждой ячейки это жестоко, если бы механизм более точечного обновления, может быть это бы помогло с производительностью, но с другой стороны без рендера каждой ячейки не добиться такого функционала. У меня ситуация, что я сделал на ее основе таблицу с инфинити скролом и при пролистывании до 100-200 страниц конечно все начинает жутко лагать. с пейджингом хоть по 50 строк более или менее нормально.
              0
              По правде говоря, не вижу я сложного функционала в их таблице. Обычная сортировка, фильтрация. Остальные примочки тоже ничего особенного. И на то, что над этим компонентом-таблицей, возможно работает целая команда, намекает как раз количество используемого кода. Можно будет поговорить об этом, когда доделаю свой вариант.
                +1
                Обычная сортировка, фильтрация.

                )))
                Фиксед колонки(справа и слева), фиксед хидер из самого сложнореализуемого. Добавляемая строчка суммы, роу- и кол- спаны, экспанд роу, автоматическая индексная колонка, колонка с чекбоксами, ресайз колонок, ширина колонок(при всей простоте, несовсем линейная реализация), ну и много можно дальше перечислять и не говоря уж о куче нужных и удобных ивентов. Так что не надо ее нивелировать — функционала просто уйма! не так просто его будет реализовать и уж точно не за несколько дней. Я только об этом. Глубокого смысла повторять не вижу, но если вам удастся повторить данный функционал и будет более производительной, то я только за.
                  0
                  Я не хотел нивелировать возможности их таблицы. Мне вообще их сборник компонентов по нраву был всегда. И внешне симпатично и функционала, хоть отбавляй. Но использовал я их компоненты довольно таки долго и в процессе использования приходили мысли о том, что мне не нравится и как сделать лучше. По крайней мере постараться сделать. Идеала ведь нет. Но стремиться хочется. А уж получится или нет, как карта ляжет )
                  P.S. ну и если серьезно, все равно не вижу ничего экстраординарного в реализации функций их таблицы.
                  Взять хотя бы упомянутые вами «фиксед колонки(справа и слева), фиксед хидер из самого сложнореализуемого» — у них таблица строится не из одного html элемента table, а из нескольких. Одна для хидера, другая для body и т.д. Отсюда и возможности фиксинга. Обычный финт ушами ) Ну и дальше можно расписать по каждой примочке. Я сам изначально подсел на Element, в том числе из-за их таблиц. Уж очень удобно. Но теперь хочется большего )
                    +1
                    Понятно что финт ушами, но это же еще реализовать надо, так же заботясь о синхронизации их обеих, если рассмотреть то же фиксед. Что и выльется в достаточно трудоемкую задачу. Я к этому) Ну флаг вам в руки, буду следить за вашей работой, посмотрим что выйдет. Хорошей продуктивной работы!
                      0
                      Спасибо!
                      Да, думаю поэтапно накатывать, попутно описывая все это действо здесь. Авось пригодится кому )
                        0
                        Вы как закончите с реализацией этого функционала — сами удивитесь сколько у вас кода прибавилось, и в итоге будет приблизительно по обьёму с оригиналом :)

                        Если конечно не выкинете не нужный вам функционал, НО, это совсем не означает что другим он тоже не нужен, хотя найдутся те, кому он тоже не надо.

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

                          0
                          Поживем — увидим.
                          Но я уверен, что кода будет меньше, как минимум из-за разного подхода к рендеру таблиц.
                          А функционал я хочу реализовать один-в-один как у Element. Чтобы сравнение было адекватное.

        Only users with full accounts can post comments. Log in, please.