Filtering tables with JavaScript

I often use this one web application that shows big tables, which quickly becomes unclear. It would be nice if I can filter the table to only show rows that contain some value I am interested in. Recently I created a plugin that allows this functionality to any HTML table.

Chrome plugin

A Chrome plugin consists of a manifest file and some JavaScript. We want our plugin to only work on pages that contain tables, so we define a page action and a background page to handle the page action functionality in the manifest.json:

"page_action": {
    "default_icon": "filter.png",
    "default_title": "Tablefilter"
},
"background": {
    "scripts": ["background.js"],
    "persistent": false
}

This adds a little button to the toolbar, which we can control from background.js. The persistent: false declaration makes sure that our page is not in memory the whole time.

In background.js, we add some code to enable the button when a table is one the page:

chrome.runtime.onInstalled.addListener(function() {
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
        chrome.declarativeContent.onPageChanged.addRules([{
            conditions: [
                // When a page contains a table tag...
                new chrome.declarativeContent.PageStateMatcher({
                    css: ["table"]
                })
            ],
            // ... show the page action.
            actions: [new chrome.declarativeContent.ShowPageAction() ]
        }]);
    });
});

Finally, we want to do something when the button is clicked:

chrome.pageAction.onClicked.addListener(function (tab) {
    chrome.tabs.executeScript({"file": "tablefilter.js"});
});

This will add tablefilter.js to the DOM of the page, making it possible for the script to add features to HTML tables.

Adding features to the table

The actual work is done in tablefilter.js. The Chrome plugin is just a way to get that script on to the page. In tablefilter.js we loop over the tables and add a new row to the top of each table. This row contains textboxes, where the user can type something to filter the table.

let tables = document.getElementsByTagName("table");
for (var table of tables) {
    decorateTable(table);
}

We use the built-in JavaScript function getElementsByTagName to get all table elements. We loop over them (with for..of) and call decorateTable for each table. That function looks like this:

function decorateTable(table) {
    let rows = table.getElementsByTagName('tr');
    let firstRow = rows[0];
    let firstCells = firstRow.children;

    let filterRow = document.createElement('tr');
    for (let i = 0; i < firstCells.length; i++) {
        let filterCell = document.createElement('td');
        let filterInput = document.createElement('input');
        filterInput.addEventListener("keyup", function () {
            filterTable(table, filterRow);
        });
        filterCell.appendChild(filterInput);
        filterRow.appendChild(filterCell);
    }

    let thead = getOrCreate(table, 'thead');
    thead.appendChild(filterRow);
}

In the first couple of lines it retrieves the cells of the first row. We use this to determine the number of columns in the table. Then it adds a new row containing a table cell with an input element for each column. It adds this row to the thead of the table.

When something is typed in one of the text boxes, the filterTable function is called. That shows only the rows that match the typed text.

function filterTable(table, filterRow) {
    let filterValues = getFilterValues(filterRow);

    let tbody = table.getElementsByTagName('tbody')[0];
    let rows = tbody.getElementsByTagName('tr');
    for (let i = 0; i < rows.length; i++) {
        let row = rows[i];
        if (isMatchingRow(row, filterValues)) {
            row.style.display = '';
        } else {
            row.style.display = 'none';
        }
    }
}

This loops over all the rows in the table body and hides or shows the row depending on whether this row matches the text in the input fields. The code that determines whether this row should be shown looks like this:

function isMatchingRow(row, filterValues) {
    let cells = row.children;
    let rowMatches = true;
    for (let c = 0; c < Math.min(filterValues.length, cells.length); c++) {
        let cell = cells[c];
        let filterValue = filterValues[c];
        let cellContent = cell.innerText;
        rowMatches &= cellContent.includes(filterValue);
    }
    return rowMatches;
}

So when the user types something in the textbox, only the matching rows are shown:

Conclusion

Using JavaScript we can quickly add some functionality to the browser and to HTML pages. This small Chrome plugin makes working with big tables a lot easier. The source for this project is available on GitHub.