preface

The content of this paper includes:

  • Element UI implementation table header table column fixed thinking and summary
  • translate3dHow to achieve table header table column fixed

In the previous article [Vue Advanced] bronze player, how to develop a set of UI library introduced the development details of Vue component library, for example to achieve the development of button, table and other components. In the Ange UI library, I implemented a table component with highly customizable content: table headers and columns can be fixed, and the content can be customized.

The first thing to admit is that the table component does a very simple job:

  • Create tables to display data
  • Fixed header
  • Table columns can be fixed
  • Can achieve a simple version of multi-level table header

Table component is one of the most complex components in the UI library. There are so many scenarios using tables in the project that it is difficult for us to cover everyone’s needs. Common ones are as follows:

  • Fixed header
  • Fix the left/right column of the table
  • Multistage header
  • Check row data
  • Expand row data
  • Data sorting

From the perspective of action objects, these requirements can be grouped into two major categories: impact layout (Eg: fixed table header column) and impact data (Eg: ticked data). In Ange UI’s Table component, only some of the functions that affect the layout class are implemented. This component does not manipulate data, and even the use of TR and TD tags (and how data is wrapped inside TD) to display data is user-defined. Click here to see examples online, or to see the code:

<ag-table offsetTop="57.5">
    <tr slot="thead">
        <! Define table header column -->
        <th v-if="isExpand">The name</th>
        <th v-for="(each, index) in singleTableHead" :key="index">{{ each }}</th>
    </tr>
    <tr v-for="(each, index) in singleTableBody" slot='tbody' :key="`tbody-${index}`">
        <! Render table contents -->
        <td v-if="isExpand">{{ each.name }}</td>
        <td>{{ each.verdict }}</td>
        <td>{{ each.song }}</td>
    </tr>
</ag-table>
Copy the code

Specify thead or TBody by slot. Simplicity means refinement and extensibility, with the added problem of high user costs (such as data selection, which ag-Table can also extend without manipulating source data).

Talk about element’s fixed header column

Reviewing the rendering of the Element Table component in a browser, Element implements a fixed table header column in the following way: The fixed part (such as table head) and the unfixed part (such as table body) split in different areas (different div), set the table body area can be rolled, and then through certain means (such as shade slot, table data backup) to synchronize the layout between different areas.

In an article on Ele. me, the implementation of fixed table header columns is described in detail. The following is a simple summary and sort out the existing problems.

1.1 The idea of fixing the header

The table component is rendered in a browser:

el-table__header-wrapper
el-table__body-wrapper

  • Two tables have different widths: there is an extra scroll bar in the area where the table body is located
  • How can column widths be kept consistent between two tables

Element does its part to address the above problem, quoting a picture from me:

Gutter elements
Pass in the width of each column

What are the downsides to this approach?

  • Extra support for added Gutter elements
  • Customizing the width of each column increases the user’s cost and ideally should be adaptive to the text content;
  • The scroll bar of the table body can not go up (roll not to the top of the table head), which makes me very worried;
  • Table header is only relative to the table body fixed, can achieve relative to the window fixed?

1.2 Fixed table column ideas

Implementing fixed table columns is relatively complex, and Element pays a “significant cost” to do so. In this fixed left and right column render:

3 form
el-table__header-wrapper
el-table__body-wrapper
el-table__fixed
el-table__fixed-right

What’s the problem with this implementation?

  • One table is rendered in triplicate, magnifying DOM overhead by a factor of three. (This is also the root cause of elementary-table page lag and performance degradation in the case of large data volume or unpaginated data)
  • Synchronize the scroll event of the mouse: Scrolling in one area requires synchronous scrolling in two other areas
  • Additional maintenance of fixed column styles and contents (such as widths, etc.)

Based on this, Ange UI’s table implementation considers another way to implement it with the lowest DOM cost.

getBoundingClientRect

The getBoundingClientRect API is used to implement a fixed table header column.

The getBoundingClientRect() method returns the size of the element and its position relative to the viewport, and its return value is a DOMRect object. The DOMRect object contains a set of read-only attributes, in pixels, that describe the border: left, right, top, and bottom. All attributes except width and height are relative to the upper-left corner of the viewport.

The diagram below:

Implement fixed table header

In a table, use thead and tbody to display the head and body of the table as follows:

<template>
  <div class="ange-table">
    <table ref="middle-table">
      <thead class="thead-middle"
             :style="theadStyle">
          <slot name="thead" />
      </thead>
      <tbody>
        <slot name="tbody" />
      </tbody>
    </table>
  </div>
</template>
Copy the code

Monitor the page scrolling event, calculate the table displacement, and reverse set the Y-axis displacement value of theAD with translate3D to achieve the effect of fixed table head. The diagram below:

Top1 (positive)
Top2 (negative)
translate3d(0px, -top2, 0px)
offsetTop
top=0 - offsetTop

export default {
  data () {
    return: {
      fixed: { / / fixed state
        top: false
      },
      clientRect: { / / displacement values
        top: 0}}},computed: {
    theadStyle () {
      const { top } = this.clientRect
      return {
        transform: `translate3d(0px, The ${this.fixed.top
            ? -top
            : 0}px, 1px)`}}},watch: {
    'clientRect.top': function (val) {
      // Start fixed when DOMRect top value is less than 0
      this.fixed.top = val < 0
    }
  },
  mounted () {
    // Listen for the page scroll event to get the DOMRect property of the table object
    window.addEventListener('scroll'.this.scrollHandle, {
      capture: false.passive: true})},methods: {
    scrollHandle () {
      const $table = this.$refs.table
      if(! $table)return

      const { top } = $table.getBoundingClientRect()
      this.clientRect.top = Math.floor(top - parseInt(this.offsetTop, 10))}}}Copy the code

In combination with the example of ag-table in @ preface, pass an offsetTop parameter in

to achieve fixed thead at the specified position. And because thead and TBody are in the same table, it doesn’t need to maintain the width of each column, so it can adapt to the content. View the demo.

Implement fixed table columns

The implementation of a fixed column requires three tables (fixing the left and right columns respectively), as follows:

<template>
  <div class="ange-table">
    <! -- left table -->
    <table v-if="hasLeftTable"
         ref="leftTable"
         :style="leftStyle">
      <thead class="thead-left"
             :style="theadStyle">
          <slot name="leftThead" />
      </thead>
      <tbody>
          <slot name="leftBody" />
      </tbody>
    </table>
    <! -- middle table -->
    <table ref="table" class="table-middle">
      <thead class="thead-middle"
             :style="theadStyle">
          <slot name="thead" />
      </thead>
      <tbody>
          <slot name="tbody" />
      </tbody>
    </table>
    <! -- right table -->
    <table v-if="hasRightTable"
           ref="rightTable"
           :style="rightStyle">
      <thead class="thead-right"
             :style="theadStyle">
          <slot name="rightThead" />
      </thead>
      <tbody>
          <slot name="rightBody" />
      </tbody>
    </table>
  </div>
</template>
Copy the code

When the table is scrolling horizontally, calculate the container’s scrolling distance scrollLeft, reverse set the X-axis displacement value of the left table with translate3D, and fix the left column; For the right table, its initial position should be set at the rightmost end of the container first, and the displacement value of X-axis should be set with scrollLeft during horizontal scrolling. The diagram below:

$rightTable.right - $container.right
scrollLeft
Initial displacement - scrollLeft

export default {
  computed: {
    leftStyle () { // The left table shifts
      const { left } = this.clientRect
      return {
        transform: `translate3d(The ${this.fixed.left
            ? left
            : 0}px, 0px, 1px)`
      }
    },
    rightStyle () { // Right side table shift
      const { right } = this.clientRect
      return {
          transform: `translate3d(${-right}px, 0px, 1px)`}}},watch: {
    'clientRect.left': function (val) {
        // If the scrolling distance is positive, set fixed
        this.fixed.left = val > 0
      }
  },
  mounted () {
    // Set its initial displacement when there is a table
    if(this.hasRightTable) {
        const container = this.$refs.container.getBoundingClientRect()
        const rightTable = this.$refs.rightTable.getBoundingClientRect()
        this.clientRect.right = Math.floor(rightTable.right  - container.right)
        // Record the initial displacement values in the right table
        this.initRight = this.clientRect.right
    }
    // Listen for the scroll event of the table container
    this.$refs.container.addEventListener('scroll'.this.scrollXHandle, {
      capture: false.passive: true
    })
    
    // ...
  },
  methods: {
    scrollXHandle () {
      // ...
      this.clientRect.left = Math.floor(this.$refs.container.scrollLeft)

      const right = Math.floor(this.initRight - this.$refs.container.scrollLeft)
      this.clientRect.right = right
    }
  }
}
Copy the code

The left and right columns are fixed according to this idea, and the effect is as follows:

Synchronous Hover effect

As the last step, since the table is composed of three tables, if the mouse hover on one table row, it needs to synchronize the hover effect on the corresponding rows of the remaining two tables. Look at the implementation of the key code:

export default {
  mounted () {
    if(this.hasLeftTable || this.hasRightTable) {
      // Define the mouse hover event
      this.$el.addEventListener('mouseover'.this.mouseOver, false)
      this.$el.addEventListener('mouseout'.this.mouseLeave, false)}},methods: {
    mouseOver (e) {
      this.hoverClass(e, 'add')
    },
    mouseLeave(e) {
      this.hoverClass(e, 'remove')
    },
    hoverClass(e, type) {
      const tr = e.target.closest('tr')
      if(! tr) {return
      }
      const idx = tr.rowIndex // Number of the current hover row
      const trs = querySelectorAll(`tbody tr:nth-child(${idx}) `.this.$el)
      if(trs.length === 0) {
          return
      }
      // Add the hover class to all tBodies with the same tr number
      trs.forEach(each= > {
          each.classList[type]('hover')})}}}Copy the code

Fixed column effect with translate3D setting the left and right column displacement to avoid:

  • Excess DOM overhead: No need to add extra DOM elements (Gutter), instead copy multiple DOM data to minimize DOM overhead.
  • There is no need to maintain column width and row height between different tables, completely adaptive;
  • There is no need to synchronize Scroll events across multiple tables

conclusion

The Table component has always been a complex component to develop, both for performance and to be as developer friendly as possible. In this, to provide another development ideas, only for the plan to develop the table component you provide a little help.

Of course, you have other ideas, welcome to comment and exchange

The end.