Blog-Archiv

Sonntag, 11. September 2022

Sortable Filterable HTML Table with ES6, Part 2

What do we need to achieve a sort- and filter-able HTML table? As usually, HTML, JavaScript (now ES6), CSS, and a HTML5-able browser. Please refer to my latest article to see what is going on now. Mind that this is client-side sorting and filtering.

Here is a template HTML page to fill out:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Sortable Filterable Table</title>

    <script>
        /* JavaScript/ES6 goes here */
    </script>

    <style>
        /* CSS goes here */
    </style>
</head>
<body>
    <!-- HTML table header and data rows go here -->
</body>
</html>

Another option would be to code everything in ES6, so that we don't need all three of HTML, CSS and ES6. Usage may become easier then, but styling the table would be more complex, and the ES6 part would become much more complex.

HTML

Here is a template HTML table to fill out:

    <table class="sortable filterable fixedColumnWidth stickyHeader bordered padded centered">
    
        <caption style="text-align: left;">
            <!-- used to display row count -->
        </caption>
        
        <thead>
            <!-- header row goes here -->
        </thead>

        <tbody>
            <!-- data rows go here -->
        </tbody>

    </table>

Don't care about the many CSS-classes the table has, I will come back to it later. Important are sortable, filterable and fixedColumnWidth only. The latter will prevent the table from changing its width dynamically when the user enters a filter and thus reduces displayed rows (→ ergonomy).

Any <thead> table column header that should be sortable and filterable must be structured like the following, so that ES6 and CSS code can build on it (the control elements are optional, leave them out if a column should not be sortable or filterable):

        <tr>
            <th>
                <button title="Click to Sort" tabindex="2"></button>
                <div>
                    <button title="Click to Filter" tabindex="1">Name</button>
                    <div>
                        <input type="text" placeholder="Name" title="Name" size="1">
                        <button title="Clear Filter"></button>
                    </div>
                </div>
            </th>

            ....
            
        </tr>

This is one column-header. The sort-button comes first, because it will be floated right relative to the column-header text. Its arrow-labels will be controlled by CSS.

Second there is a DIV wrapping another button and another DIV. The button will render the column-header text and open a filter-textfield as soon as it gets clicked or ENTERed.

The nested DIV contains the initially hidden filter-textfield and a clear-button for it. This will be positioned absolute, so the clear-button needs not to be before the input field.

Everything else is done in ES6 or CSS. Anyway title tooltips are defined here, the column-header text is duplicated as placeholder text for the filter-textfield, and a TAB-key focus-order is declared. The size attribute finally is here to prevent the text field from taking a default width, which may expand into the rightmost column and get invisible when overflow was set to hidden. Yes, layout with HTML and CSS is that complicated.

Here is the complete table HTML, including 5 sample rows:

    <table class="sortable filterable fixedColumnWidth stickyHeader bordered padded centered">
    
        <caption style="text-align: left;">
        </caption>
        
        <thead>
        <tr>
            <th>
                <button title="Click to Sort" tabindex="2"></button>
                <div>
                    <button title="Click to Filter" tabindex="1">Name</button>
                    <div>
                        <input type="text" placeholder="Name" title="Name" size="1">
                        <button title="Clear Filter"></button>
                    </div>
                </div>
            </th>
            <th class="num" aria-sort='descending'> <!-- pre-sorted column -->
                <button title="Click to Sort" tabindex="4"></button>
                <div>
                    <button title="Click to Filter" tabindex="3">Höhe</button>
                    <div>
                        <input type="text" placeholder="Höhe" title="Höhe" size="1">
                        <button title="Clear Filter"></button>
                    </div>
                </div>
            </th>
            <th>
                <button title="Click to Sort" tabindex="6"></button>
                <div>
                    <button title="Click to Filter" tabindex="5">Gruppe</button>
                    <div>
                        <input type="text" placeholder="Gruppe" title="Gruppe" size="1">
                        <button title="Clear Filter"></button>
                    </div>
                </div>
            </th>
            <th>
                <button title="Click to Sort" tabindex="8"></button>
                <div>
                    <button title="Click to Filter" tabindex="7">Land</button>
                    <div>
                        <input type="text" placeholder="Land" title="Land" size="1">
                        <button title="Clear Filter"></button>
                    </div>
                </div>
            </th>
            <th>
                Ö-Karte Nr.
            </th>
        </tr>
        </thead>

        <tbody>
        <tr>
            <td>Großglockner</td>
            <td class='num'>3798</td>
            <td>Hohe Tauern Glocknergruppe</td>
            <td>Grenze Kärnten mit Tirol</td>
            <td>153 - u</td>
        </tr>
        <tr>
            <td>Wildspitze</td>
            <td class='num'>3770</td>
            <td>Ötztaler Alpen</td>
            <td>Tirol</td>
            <td>173 - o</td>
        </tr>
        <tr>
            <td>Weißkugel</td>
            <td class='num'>3738</td>
            <td>Ötztaler Alpen</td>
            <td>Grenze Tirol mit Italien</td>
            <td>172 - u</td>
        </tr>
        <tr>
            <td>Glocknerwand</td>
            <td class='num'>3722</td>
            <td>Hohe Tauern Glocknergruppe</td>
            <td>Grenze Kärnten mit Tirol</td>
            <td>153 - u</td>
        </tr>
        <tr>
            <td>Großvenediger</td>
            <td class='num'>3674</td>
            <td>Hohe Tauern Venedigergruppe</td>
            <td>Grenze Tirol mit Salzburg</td>
            <td>152 - u</td>
        </tr>
        </tbody>

    </table>

Before introducing ES6 code, which I will do in a separate article, here is CSS for the sort- and filterable table.

CSS

You can decide to make your table sortable but not filterable, or filterable but not sortable. Just use the class attribute of the <table> element. Sortable-only tables do not need the fixedColumnWidth class nor the related ES6 code.


Following CSS is to be written into a <style> element (or an external CSS-file) for both sorting and filtering:

    <style>
    /** both sortable and filterable */
	
    /** align numbers to the right */
    table.sortable td.num,
    table.filterable td.num {
      text-align: right;
    }
    
    /** header cells */
    table.sortable thead th,
    table.filterable thead th {
      white-space: nowrap; /* keep all header labels in just one line */
      text-align: left; /* do not horizontally align to center */
    }

    /** layout container for label: take all space left of sort-button */
    table.sortable thead th > div,
    table.filterable thead th > div {
      overflow: hidden;
    }

    /** label text in column header cells */
    table.sortable thead th > div > button,
    table.filterable thead th > div > button {
      display: block;
      background: transparent;
      border: 1px solid transparent;
      width: 100%;
      cursor: pointer;
      font-size: inherit;
      font-weight: inherit;
      text-align: left;
    }

    </style>

Numeric contents normally are rendered right-aligned. The num CSS-class is used to indicate a numeric column. This is important for sorting, and plays a role also when filtering (searching by "starts-with").

All column headers should be one-lined to avoid layout problems with nested elements. The white-space: nowrap CSS does this. Table header cells are centered by default, I want them left-aligned.

The overflow: hidden statement is for the nested DIV. Without this, the nested "Name" label button would not take all space to the left of the sort-button.

The last block is about styling the "Name" label button to look like a normal column header label, and receive clicks on all space from left to right before the sort-button.


Following CSS is to be included for sorting:

    <style>
    /** sortable */

    /** don't show filter fields when not filterable */
    table.sortable:not(.filterable) thead th > div > div {
      display: none;
    }
    
    /** the sort-buttons in table header */
    table.sortable thead th > button {
      display: block;
      float: right;
      background: transparent;
      border: none;
      cursor: pointer; /* show click acceptance */
      margin-top: 2px;
    }
    
    /** sort ascending pseudo-element */
    table.sortable thead th[aria-sort="ascending"] > button::after {
      content: "\0025B2"; /* ▲ */
    }
    
    /** sort descending pseudo-element */
    table.sortable thead th[aria-sort="descending"] > button::after {
      content: "\0025BC"; /* ▼ */
    }
    
    /** pseudo-element expressing "not sorted by this column" */
    table.sortable thead th:not([aria-sort]) > button::after {
      content: "\002662"; /* ♢ */
    }
    
    </style>

CSS allows negation of classes via the :not() pseudo-class. This is used here to declare that the nested filer-DIV should not be visible when no filterable CSS-class was declared on the <table> element.

The sort button gets floated to the right of the following element. A display: block layout is OK for floated elements, it gives us the opportunity to size it. The button is styled to melt into the background, and aligned to horizontally fit the label text.

The last three blocks declare the arrow-labels of the sort button according to the value of the aria-sort attribute. ES6 will be responsible for setting that attribute to either "descending" or "ascending", or for removing it to activate the rule th:not([aria-sort]).


Following is to be included for filtering:

    <style>
    /** filterable */
    
    /** don't show sort buttons when not sortable */
    table.filterable:not(.sortable) thead th > button {
      display: none;
    }

    table.filterable thead th > div > button:hover,
    table.filterable thead th > div > button:focus {
      border: 1px solid gray;
    }
    
    /** layout container for text-field and its clear-button: take all space left of sort-button */
    table.filterable thead th > div > div {
      display: none;
      position: relative;
    }

    /** filter field in header cells */
    table.filterable thead th > div > div > input[type='text'] {
      width: 100%;
      box-sizing: border-box;
      padding: 0;
      padding-right: 12px; /* leave place for clear-button */
      font-size: inherit;
    }
    
    /** The clear-button for filter field */
    table.filterable thead th > div > div > button {
      position: absolute;
      top: 0;
      right: 0;
      border: none;
      background: transparent;
      cursor: pointer;
      font-size: inherit;
    }
    
    /** icon for clear-button */
    table.filterable thead th > div > div > button::after {
      content: "\00d7"; /* multiply */
    }

    </style>

A table that is filterable but not sortable should not display the sort-button.

A gray border should appear over the column text label button when the mouse is over, or it has the input-focus (click or TAB-key).

The nested DIV containing the textfield and the clear button should not be shown initially. Its position is set to relative to be parent for absolutely positioned children. No, CSS layout is not intuitive.

The filter textfield gets shortened by padding-right 12 pixels to leave place for the absolutely positioned clear-button right of it. Thus it can expand to 100% of the available horizontal space, the button will sit over it.

The clear-button is positioned absolute inside its parent. It gets styled to melt into the background.

The clear-button gets its label text. I prefer using UNICODE letters instead of image files that need to be managed.


Here are some general CSS class implementations useful with HTML tables:

    <style>
    /** general table styles */
    
    table.centered {
      margin-left: auto;
      margin-right: auto;
    }
    
    table.bordered  {
      border-collapse: collapse;
    }
    
    table.bordered td, table.bordered th  {
      border: 1px solid gray;
    }
    
    table.padded td, table.padded th  {
      padding: 0.6em;
    }

    table.stickyHeader thead {
      position: sticky;
      inset-block-start: 0;
      background-color: #ddd;
    }    

    </style>

If you want to horizontally center your table in its layout container, you can include the centered CSS class.

Next two statements draw a thin gray border around any cell, collapsing colliding borders. Use this by including the CSS class bordered.

The padding of cells make the table bigger, but also more user-friendly. Customize your padding and include it as CSS class padded.

The sticky value for position has been added recently, old or badly maintained browsers will not support this. It makes the table header stay visible even while the long table body gets scrolled down. I experimented with a border-bottom to separate it from the table content, but this did not work, so I gave it a gray background color instead.


That's all for HTML and CSS. Now ES6 should give life to the page. I will roll it out in Part 3 of this article series. The ES6 implementation of CSS-class "fixedColumnWidth" will be contained.




Keine Kommentare: