preface

In the background management system development process, the use of buttons, input boxes, forms and other controls is very high. So when the component library controls do not meet our business needs, we need to change the components. This article is my development process for the Table component of some thinking.

The body of the

When the format length of data source cannot be determined, the situation as shown in the figure above often occurs. The table is irregular and the height of the table is elongated due to a large number of data. Instead of 20 table data, only 10 table data can be displayed on one page. If the Age field is the key information, will it affect our look and feel if we display it like this? A series of problems will make our system become very bad. I won’t talk about the negatives, but let’s get right to the point.

Let me give you some preliminary thoughts: when we can determine the approximate width of a column in a table, we try to specify the width. So a lot of times, we can’t be certain about the width of certain fields. At this point, how do we deal with it?

Let’s take a look at the effect after preliminary treatment:

After determining the width of the Name and Age columns, we let the remaining Address and Tags with uncertain column widths be given appropriate widths according to our algorithm. When the content in Address or Tags is long, it automatically adds ellipses, and when we mouse over, it displays the tooltip to see all the content.

So how did we do it?

We encapsulated the Table component of ANTD with another layer, and processed the render of columns, judging whether the tooltip needs to be displayed according to the value of showOverflowTooltip on column.

Of course, the most important thing here is the calculation of the so-called appropriate width:


const columns = [
  {
    title: 'Name'.    dataIndex: 'name'. key: 'name'. width: 150  },  {  title: 'Age'. dataIndex: 'age'. key: 'age'. width: 150  },  {  title: 'Address'. dataIndex: 'address'. key: 'address'. },  {  title: 'Tags'. key: 'tags'. dataIndex: 'tags'  }, ];  Copy the code

When we set the width of Name and Age to 150, we can see in the figure above that the width of th is 182px. The extra 36px is the sum of the left and right padding values. The total width of the Table can be obtained by accessing the DOM. So 1615px-182px-182px = 1251px. So how do we allocate 1251px? It’s easier to just average it out, because we’re not sure what its approximate width is. Here is the average score algorithm:

  • Get the width of the Table
  • Calculate the total width of the column whose width has been defined. Since the TABLE TD of ANTD has its own padding 16px on the left and 16px on the right, we need to add 32px when calculating
  • Count the number of unset columns
  • Calculate the appropriate width for the unset column: divide the allocated width by the number of unset columns

  const columnStyle = {
      whiteSpace: 'nowrap'.      overflow: 'hidden'.      textOverflow: 'ellipsis'
 };  .  const arr = []; // Define an array to store the new column  const totalWidth = document.getElementById(uuid) ? document.getElementById(uuid).clientWidth : 80; // Calculate the width of the Table  const hasWidth = props.columns.reduce((pre,cur) = > { return (cur.width + 32 || 0) + pre }, 0); // Calculate the total width (32 is 16 before and after each TD content)  const hasNoWidthNumber = props.columns.filter(item= >! item.width).length;// Calculate the number of columns without width set  const columnWidth = parseInt((totalWidth - hasWidth) / hasNoWidthNumber, 10) - 32; // Calculate the width of column   props.columns.forEach(element= > {  if(! element.render) { element['render'] = ele= > {  if (element.showOverflowTooltip === false) {  return ele;  }  return (  <Tooltip placement="leftTop" title={ele}>  <div  style={{  width: element.width || columnWidth. . columnStyle  }}  >  {ele}  </div>  </Tooltip>  );  };  }  arr.push(element);  }); Copy the code

It seems that our original problem has been solved. So let’s think about the question, what is the key point to achieve this effect? ColumnWidth is the calculation of the table content width. The value of columnWidth is dependent on the current Table width, so when we stretch the Table, the Table width and height will also change. Should the columnWidth value change as well? This leads to a problem. We need to listen to the Table size in real time. Isn’t that a performance drain? One more question, is it necessary to render the Tooltip multiple times? Why doesn’t the Tooltip work when we want to view it? As soon as we think about it, we will find that the Table still has a lot of problems, it becomes not Nice again.

Let’s list the problem points:

  • Dynamically calculated widths affect performance
  • There is no need to render redundant Tooltip components

After referring to some excellent component libraries at home and abroad, I found that some component libraries have implemented the function of this Table. Let’s take the familiar Table component of Element-UI as an example to analyze how it deals with the Table data overflow problem.

Let’s look at a piece of code:

  <table
    class="el-table__body"
    cellspacing="0"
    cellpadding="0"
    border="0">
 <colgroup>  {  this.columns.map(column => <col name={ column.id } key={column.id} />)  }  </colgroup>  <tbody>  {  data.reduce((acc, row) => {  return acc.concat(this.wrappedRowRender(row, acc.length));  }, [])  }  <el-tooltip effect={ this.table.tooltipEffect } placement="top" ref="tooltip" content={this.tooltipContent} ></el-tooltip>  </tbody>  </table> Copy the code

We found that it nested a tooltip component in the body of the table content. As you can probably guess from this code, the tooltip must be controlled by one of the actions, and the display value should be assigned at that time.

At this point, we just need to find where tooltipContent is assigned. Sure enough, we found a method called handleCellMouseEnter, which we can easily guess from the name, should handle some event when the mouse moves over the table cell.


  handleCellMouseEnter(event, row) {
    const table = this.table;
    const cell = getCell(event);

.  // Check whether text-overflow exists and display tooltip if so  const cellChild = event.target.querySelector('.cell');  if(! (hasClass(cellChild,'el-tooltip') && cellChild.childNodes.length)) {  return;  } . const range = document.createRange();  range.setStart(cellChild, 0);  range.setEnd(cellChild, cellChild.childNodes.length);  const rangeWidth = range.getBoundingClientRect().width;  const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) | |0) + (parseInt(getStyle(cellChild, 'paddingRight'), 10) | |0);  if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {  const tooltip = this.$refs.tooltip;  this.tooltipContent = cell.innerText || cell.textContent; . this.activateTooltip(tooltip);  }  }  Copy the code

When we look at the handleCellMouseEnter method, we see another surprise: he calculates the width here as well, and after doing some calculations with that width, he does some processing with the Tooltip.

When I saw how the big guy calculated the width, and then looked at the time of the submission (2 years ago), I was left in tears with no technology. No matter what, tears also want to continue to finish watching.

For those of you who aren’t quite sure what document.createrange () means. So here’s a quick rundown:

The Range object represents a contiguous Range of documents.

Let’s take a few examples to impress you:

Example 1:

<p id="p1"><span>hello</span>world</p>
Copy the code
var range1 = document.createRange(),
    range2 = document.createRange(),
    p1 = document.getElementById("p1");
    range1.selectNode(p1);
    range2.selectNodeContents(p1);
Copy the code
              range1
                |
|-----------------------------------|
|                                   |
<p id="p1"> <span>hello</span>world </p>
 | |  |-----------------------|  |  range2 Copy the code

The difference between selectNodes and selectNodeContents is in the choice of boundary points.

Example 2:

<p id="p2">hello World!</p>
Copy the code
range = document.createRange();
p2 = document.getElementById("p2").childNodes[0];
range.setStart(p2,1);
range.setEnd(p2,8)
Copy the code
             range
               |
             |----|
             |    |
<p id="p2">hello World!</p>
Copy the code

The content selected by Range is LLO Wo.

Now that we know about the range object, let’s look at these two lines of code:

  const cellChild = event.target.querySelector('.cell'); // Get the table cell DOM
  range.setStart(cellChild, 0);
  range.setEnd(cellChild, cellChild.childNodes.length);
Copy the code

We can easily tell that range selects the data source.

OK! So let’s move on, now that we have the continuous range of the range, we have to figure out the width of the range.

  const rangeWidth = range.getBoundingClientRect().width;
Copy the code

GetBoundingClientRect () = getBoundingClientRect(); getBoundingClientRect() = getBoundingClientRect(); getBoundingClientRect() = getBoundingClientRect(); Returns a DOMRect object that qualifies the contents of the selected document object. This method returns a rectangle enclosing the set of bounding rectangles for all elements in the document object. Its width property represents the width of the element (the content of the selected document object) in the browser. I guess there’s no update on w3c because some browsers aren’t very well supported. After all, the authorities also gave a warning.

This is an experimental feature that is still under development in some browsers, please refer to the browser compatibility table to see which prefixes are suitable for use in different browsers. The syntax and behavior of this feature may change in future versions of browsers as the standard documentation for this feature may be revised.

Let’s take a look at getBoundingClientRect support in modern browsers:

As you can see, browser support is pretty good, even IE is up to 9. Since browser support is so good, we can rest assured. Let’s move on to the rest of the code

 const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) | |0) + (parseInt(getStyle(cellChild, 'paddingRight'), 10) | |0);
Copy the code

This code should get the table cell padding value

if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
  const tooltip = this.$refs.tooltip;
  this.tooltipContent = cell.innerText || cell.textContent;
.  this.activateTooltip(tooltip);
} Copy the code

This code determines whether the tooltip is displayed based on the width of the cell’s contents. When the length of the data in the cell is greater than the actual length of the cell, the tooltip is displayed, and the content is assigned at this point.

So far our two problem points have been basically solved. You can calculate the width of the cell content as the mouse moves in, and then compare the width of the content to the actual width of the cell to determine if you need to display the tooltip. We can see that the Element-UI is great for some of the details of the design. Give it a thumbs up.

At the end

This article is just a snapshot of my thoughts during the development process, and I welcome your comments.