📜 ⬆️ ⬇️

Tables! Tables? Tables ...

In the article I will show the standard table layout, what alternatives it has. I will give an example of my own table and markup, and also describe the general points of its implementation.


Standard HTML4 table


When there was a need for HTML markup to show tables - they invented the <table> .
What gives us the table in the browser? Here are some basic features:



In this case, the percentage ratio of each column to the total width is calculated and each column is stretched according to the percentage ratio.


In the first example, the width of the entire table (approximately) = 387px , columns Company = 206px , columns Contact = 115px .


In percentage, Company = 206px/387px * 100% = 53% , Contact = 115px/387px * 100% = 30% .


Now when the contents of the table are stretched , the width of the entire table (approximately on my screen) = 1836px , columns Company = 982px , columns Contact = 551px .


In percentage, Company = 982px/1836px * 100% = 53% , Contact = 551px/1836px * 100% = 30% .



You can "finish" the table by specifying the CSS property table-layout: fixed . Description of the property.


So we break the auto-tuning of the table width and now the table obeys the specified widths for each column (or the entire table), but the table fits exactly into the specified width.


If we did not specify the width of the columns, then with a “broken” table , the = / .



Use standard table


In all the examples above, in the table layout I used abbreviated markup:


Abbreviated markup
 <table> <tr> <th>Header 1</th> <th>Header 2</th> </tr> <tr> <td>1.1</td> <td>1.2</td> </tr> <tr> <td>2.1</td> <td>2.2</td> </tr> </table> 

However, you can use "canonical" markup:


Canon markup
 <table> <thead> <tr> <th>Header 1</th> <th>Header 2</th> </tr> </thead> <tbody> <tr> <td>1.1</td> <td>1.2</td> </tr> <tr> <td>2.1</td> <td>2.2</td> </tr> </tbody> </table> 

If you need a table without a header and at the same time, we need to control the width of the columns:


Layout without caps
 <table> <tbody> <colgroup> <col width="100px"></col> <col width="150px"></col> </colgroup> <tr> <td>1.1</td> <td>1.2</td> </tr> <tr> <td>2.1</td> <td>2.2</td> </tr> </tbody> </table> 

Most often we need to get the following in the markup. We have a container with a given width or with a given maximum width. Inside it we want to enter a table.


If the width of the table is greater than the container, then it is necessary to show the scroll for the container. If the width of the table is less than the container, then it is necessary to expand the table to the width of the container.


But in no case do we want the table to make our container wider than we were asked.


Using this link you can see the container with the table in action. If we narrow the container, then at the moment when the table can no longer narrow - a scroll will appear.


Table adjustment


Setting the width of the table and columns


The first dilemma faced by front-end developers is to set or not to set the width of the columns.


If you do not specify, then the width of each column will be calculated depending on the content.


Based on logic, you can understand that in this case, the browser needs two passes. At first, it simply displays everything in the table, calculates the width of the columns (min, max). The second adjusts the width of the columns depending on the width of the table.


Over time, you will be told that the table looks ugly, because one of the columns is too wide and


            ,     

And the most common "feature":



Those. if the text in the cell climbs over the width of the column, then it must be reduced and at the end added ...


The first disappointment is that if you do not set the width of the columns, the reduction does not work. This has its own logic, because on the first pass, the browser calculates the min / max column width without a reduction, and here we are trying to shorten the text. You must either recalculate everything or ignore the abbreviation.


The reduction is simple; you need to specify the CSS properties for the cell:


CSS
 td { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } 

And accordingly set the width of the column. From this link you can see that everything is configured, but the abbreviation does not work.


There is a note in the specification explaining a little why the abbreviation does not work:


 If column widths prove to be too narrow for the contents of a particular table cell, user agents may choose to reflow the table 

Again, the table will narrow to the minimum width of the content. But if you apply the table-layout: fixed property, the table will begin to "obey" and the reduction will work. But auto-tuning column width is no longer working.


Table scroll setting


The above example will work with a scroll and you can use it. However, the following requirement arises:


    ,      ,    

The second dilemma faced by front-end developers:



In the specification of the table there is a direct indication that the table body can be with a header and a basement. Those. hat and basement are always visible.


 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 

And there is an indication that the table body can be scrolled, and the cap and basement will remain in place:


 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 

And in fact, browsers do not do this and the scrolling for the table must be created / configured manually.


There are many ways to do this, but they all boil down to the fact that:


  1. we do not create additional markup and try to fasten the scroll to what it is (to the table body, or wrap it in a container, and make the value of the cells in the header absolutely positioned )

You can set a limited height for the table body. The following example shows that you can try to set the height of the table body .
As a result, we break the table display of the body of the CSS table by the display: block property, and at the same time it is necessary to synchronize the scrolling of the header with the table body.


  1. we create additional markup (composite tables) and then when scrolling the original we synchronize additional markup

This option is where everyone proposes / builds solutions.


Examples of composite tables


If we need to scroll the body of the table, then we cannot do without the composite markup. All examples of composite tables use their own custom markup.


One of the most famous Data Tables tables uses the following markup:


HTML Data Tables
 <div class="dataTables_scroll"> <div class="dataTables_scrollHead"> <div class="dataTables_scrollHeadInner"> <table> <thead> <tr> <th></th> <th></th> <th></th> </tr> </thead> </table> </div> </div> <div class="dataTables_scrollBody"> <table> <thead> <tr> <th><div class="dataTables_sizing"></div></th> <th><div class="dataTables_sizing"></div></th> <th><div class="dataTables_sizing"></div></th> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> </tr> </tbody> </table> </div> </div> 

I deliberately shorten the markup so that I can get a general picture of how the markup looks.


We see two tables in the markup, although for the user this is “seen” as one.
The following example of React Bootstrap Table , if you look at the markup, also uses two tables:


React Bootstrap Table HTML
 <div class="react-bs-table-container"> <div class="react-bs-table"> <div class="react-bs-container-header table-header-wrapper"> <table class="table table-hover table-bordered"> <colgroup><col class=""><col class=""><col class=""></colgroup> <thead> <tr> <th></th> <th></th> <th></th> </tr> </thead> </table> </div> <div class="react-bs-container-body"> <table class="table table-bordered"> <colgroup><col class=""><col class=""><col class=""></colgroup> <tbody> <tr class=""> <td></td> <td></td> <td></td> </tr> </tbody> </table> </div> </div> </div> 

The upper table shows the header, the lower - the body. Although it seems to the user as if this is a single table.


Again, the example uses scrolling synchronization; if you scroll the body of the table, the header will synchronize.


But how is it that the body of the table (one table) and the heading (another table) adapt to the width of the container and they do not disperse in any way along the width and coincide with each other?


Here, who both knows how and synchronizes, for example, here is the width synchronization function from the above library:


_adjustHeaderWidth
 componentDidUpdate() { ... this._adjustHeaderWidth(); ... } _adjustHeaderWidth() { ... //           ,    <col>    //      } 

There is quite a logical question, but why then use the <table> tag at all if you only use the width auto-tuning from the standard table?


And here we are not the first, some do not use tabular markup at all. For example, Fixed Data Table or React Table .


The markup in the examples is something like this:


Markup
 <div class="table"> <div class="header"> <div class="row"> <div class="cell"></div> <div class="cell"></div> </div> </div> <div class="body"> <div class="row"> <div class="cell"></div> <div class="cell"></div> </div> </div> </div> 

Hence the name fixed table , i.e. for such markup, we must specify in advance the width of all the columns (the width of the table, and sometimes the height of the row). Although if we want to shorten the text, it is still necessary to set the width of the columns, even in a regular table.


The following Reactabular table uses an interesting approach in sync.


The author went further and made scrollable not only the body, but also the head of the table. In browsers that show the scrolling slider - it looks awful, but in touch browsers are very cool and functional.


If we scroll the body of the table, then the header synchronizes, and if we scroll the header, then the body synchronizes.


But how to make auto-tuning of the column width in the composite table you ask? Here is an interesting way to use an additional browser pass. For example, in this ag Grid table, you can automatically calculate the appropriate column width.


The code has an auto-tuning function for the column width :


getPreferredWidthForColumn
 public getPreferredWidthForColumn(column: Column): number { //  <span style="position: fixed;"> //       //   span ( ) //  <span style="position: fixed;"> } 

Implementing your own table


It turns out that the composite table requires additional synchronization between the parts, so that for the user it all seemed like one table.


All composite tables (and mine) suffer from a disadvantage, they do not have a standard for how to customize / customize them (and this is logical, because the implementation refused HTML4 tables).


When you start to study one composite table, then you begin to spend time customizing it.


Then for another project, you study another table (for example, when switching from Angular1 to React, or from jQuery to Vue), and customization is completely different.


A logical question arises, is it worth the time spent? Should we learn a bunch of framework-table again and again?


Maybe it is easier to master the basic moments of a composite table for yourself and then you can make your table on any framework (Angular / React / Vue / future ...)? For example, on your table you will spend 2 days at the start, then customize within 30 minutes.


And you can connect a ready-made framework for 30 minutes and then customize each feature in 1 day.


To premera, I will show how to make your composite table on React.


The table will be:



Next will be an explanation of only some aspects of the development, you can immediately see the result .


Markup


For the markup we will use div elements. If you use display: inline-block for cells, then there will be the following markup:


Inline block HTML
 <div class="row"> <div class="cell" style="width: 40px; display: inline-block;"></div> <div class="cell" style="width: 40px; display: inline-block;"></div> </div> 

But there is one problem - the browser (not all browsers) interprets the empty spaces between the cells as text nodes.


There is a great article on how to deal with it.


And if we use a templating engine (EJS, JSX, Angular, Vue), then this is easy to solve:


inline block fixed HTML
 <div class="row"> <div class="cell" style="width: 40px;">{value}</div><div class="cell" style="width: 40px;">{value}</div> </div> 

However, already in 2017, flexbox has long been supported, I did projects on it back in 2014 for IE11.


And today you can not be ashamed at all. This will simplify the task for us, it will be possible to make as many empty nodes as needed:


Flexbox HTML
 <div class="row" style="display: flex; flex-direction: row;"> <div class="cell" style="width: 40px; flex: 0 0 auto;">{value}</div> <!--    --> <div class="cell" style="width: 40px; flex: 0 0 auto;">{value}</div> </div> 

Common points of use


The table must be embedded in the Redux architecture, examples of such tables suggest connecting their reducers .


I don't like this approach. In my opinion, the developer must control the sorting and filtering process. This requires additional code.


Instead of such a “black box”, which is then difficult to customize:


Normal connection
 render() { return ( <div> <Table filter={...} data={...} columns={...} format={...} etc={...} /> </div> ) } 

the developer will have to write:


Own connection
 render() { const descriptions = getColumnDescriptions(this.getTableColumns()), filteredData = filterBy([], []), sortedData = sortBy(filteredData, []); return ( <div> <TableHeader descriptions={descriptions} /> <TableBody data={sortedData} descriptions={descriptions} keyField={"Id"} /> </div> ) } 

The developer himself must prescribe the steps: calculate the description of the columns, filter, sort.


All functions / constructors getColumnDescriptions, filterBy, sortBy, TableHeader, TableBody, TableColumn will be imported from my table.


An array of objects will be used as data:


Sample data
 [ { "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" }, ] 

I liked the approach of creating column descriptions in jsx as elements.


We will use the same idea, however, to make the head and body of the table independent, we will calculate the description once and pass it both to the head and body:


Description of columns and connection
 getTableColumns() { return [ <TableColumn row={0} width={["Company", "Cost"]}>first header row</TableColumn>, <TableColumn row={1} dataField={"Company"} width={200}> Company </TableColumn>, <TableColumn row={1} dataField={"Cost"} width={100}> Cost </TableColumn>, ]; } render() { const descriptions = getColumnDescriptions(this.getTableColumns()); return ( <div> <TableHeader descriptions={descriptions} /> <TableBody data={[]} descriptions={descriptions} keyField={"Id"} /> </div> ) } 

In the getTableColumns function getTableColumns we create a column description.


I can describe all the required properties through propTypes , but after they have been taken to a separate library, this solution seems dubious.


Be sure to specify row - a number that indicates the index of the row in the header (if the header will be grouped).


The dataField parameter determines which key from the object to use to get the value.


The width also a required parameter, it can be specified as a number or as an array of keys on which the width depends.


In the example, the top row in the row={0} table depends on the width of the two columns ["Company", "Cost"] .


The TableColumn element is TableColumn , it will never be displayed, but its contents this.props.children is displayed in the header cell.


Development


Based on the descriptions of the columns, we will make a function that will split the descriptions by rows and by keys, and also will sort the descriptions by rows in the resulting array:


getColumnDescriptions
 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; } 

Now the processed descriptions are transferred to the header and to the body to display the cells. The cap will build the cells like this:


Header render
 getFloor(width, factor) { return Math.floor(width * factor); } renderChildren(descriptions) { const {widthFactor} = this.props; return descriptions.map(rowDescription => { return <div className={styles.tableHeaderRow} key={rowDescription.key}> {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 <div className={styles.tableHeaderCell} key={dataField || index} style={{ width: _width + 'px' }}> {cellDescription.props.children} </div> })} </div> }) } render() { const {className, descriptions} = this.props; return ( <div className={styles.tableHeader} ref={this.handleRef}> {this.renderChildren(descriptions)} </div> ) } 

The table body will also build cells based on the processed column descriptions:


Body render
 renderDivRows(cellDescriptions, data, keyField) { const {rowClassName, widthFactor} = this.props; return data.map((row, index) => { return <div className={`${styles.tableBodyRow} ${rowClassName}`} key={row[keyField]} data-index={index} onClick={this.handleRowClick}> {cellDescriptions.map(cellDescription => { const {props} = cellDescription; const {dataField, dataFormat, cellClassName, width} = props; const value = row[dataField]; const resultValue = dataFormat ? dataFormat(value, row) : value; return <div className={`${styles.tableBodyCell} ${cellClassName}`} key={dataField} data-index={index} data-key={dataField} onClick={this.handleCellClick} style={{ width: this.getFloor(width, widthFactor) + 'px' }}> {resultValue ? resultValue : '\u00A0'} </div> })} </div> }); } 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 ( <div className={`${styles.tableBody} ${className}`} ref={this.handleRef}> {this.renderDivRows(cellDescriptions, data, keyField)} </div> ) } 

The table body uses descriptions that have a dataField property, so descriptions are filtered using the getCellDescriptions function.


The table body will listen for screen resizing events as well as scrolling the table body itself:


Listeners
 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); } } 

Adjustment of the width of the table is as follows.


After the display, the width of the container is taken, compared to the width of all the cells; if the width of the container is larger, the width of all the cells increases.


To do this, the developer must store the state of the width coefficient (which will change).


The following functions are implemented in the table, but the developer can use their own. To use already implemented ones, you need to import them and link them to the current component:


Linking
 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'); } 

Width adjustment function:


adjustBody
 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
 adjustScroll(e) { const {handleAdjustScroll} = this.props; if (typeof handleAdjustScroll === 'function') { handleAdjustScroll(e); } } 

redux — , ( , , ).


adjustBody adjustScroll — .


TableColumn jsx . : , .


/ .


condition
 this.state = { activeSorts: [], activeFilters: [], }; 

/:


getTableColumns
 getTableColumns() { const {activeFilters, activeSorts, columnsWidth} = this.state; return [ <TableColumn row={0} width={["Company", "Cost"]}>first header row</TableColumn>, <TableColumn row={1} dataField={"Company"} width={300}> <MultiselectDropdown title="Company" activeFilters={activeFilters} dataField={"Company"} items={[]} onFiltersChange={this.handleFiltersChange} /> </TableColumn>, <TableColumn row={1} dataField={"Cost"} width={300}> <SortButton title="Cost" activeSorts={activeSorts} dataField={"Cost"} onSortsChange={this.handleSortsChange} /> </TableColumn>, ]; } 

SortButton MultiselectDropdown "" /, . activeSorts activeFilters , .


, , .


:



. , , — .


.


')

Source: https://habr.com/ru/post/331494/


All Articles