Range Filter based on data-* attributes

Range Filter based on data-* attributes

heronalexheronalex Posts: 3Questions: 1Answers: 0

I would like to add a range filter at the bottom of a column, except that instead of filtering on the column exact content, I would like to filter on data- attributes.

The reason is that the cell value is textual (eg 'New Kingdom', 'Old Kingdom', 'First Intermediate Period'), but it corresponds to a numerical range of dates.

I manage to add two search boxes, with the same model as the simple text search by column (https://datatables.net/examples/api/multi_filter.html), but I struggle to find how to access the attributes. Is this possible?

Bonus question, if it is possible: what is the best way to deal with unknown values? Empty data attributes?

Simplified example to illustrate the problem:

<table id="example" class="display">
        <thead>
            <tr>
                <th>Identifier</th>
                <th>Title</th>
                <th>Material</th>
                <th>Period</th>
                <th>FindPlace</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>item1</td>
                <td>My first item</td>
                <td>papyrus</td>
                <td data-from="-1279" data-to="-1213">New Kingdom</td>
                <td>Hermopolis</td>
            </tr>
            <tr>
                <td>item2</td>
                <td>My second item</td>
                <td>pottery</td>
                <td data-from="-332" data-to="565">Graeco-Roman Period</td>
                <td>Diospolis Magna</td>
            </tr>
            <tr>
                <td>item3</td>
                <td>My third item</td>
                <td>stone</td>
                <td data-from="" data-to="">Unknown</td>
                <td>Unknown</td>
            </tr>
        </tbody>
</table>

And the associated javascript:

let table = new DataTable('#example', {
    searchHighlight: true,
    columnDefs: [
        { className: 'select-filter', targets: [2] }, // add select filter material
        { className: 'range-filter', targets: [3]}, // add from/to range filter for dates
        { className: 'search-filter', targets: [4] } // add search filter for findplace
    ]
    initComplete: function () {
        // add filter boxes for filtering individual columns (eg material), this works well
        this.api()
            .columns('.select-filter')
            .every(function () {
                let column = this;
                // name of the filter is the column header + 'filter'
                let name = column.header().innerText+' filter:';
                // identifier is 'select-' + the column index
                let identifier = 'select-'+column.index();
                // Create select element
                let select = document.createElement('select');
                select.setAttribute('id', identifier);
                select.add(new Option(name,''));
                column.footer().replaceChildren(select);
 
                // Apply listener for user change in value
                select.addEventListener('change', function () {
                    column
                        .search(select.value, {exact: true})
                        .draw();
                });
 
                // Add list of options
                column
                    .data()
                    .unique()
                    .sort()
                    .each(function (d, j) {
                        select.add(new Option(d));
                    });
            });
        // add search box for individual column (e.g. findplace), this works well
        this.api()
            .columns('.search-filter')
            .every(function () {
                let column = this;
                let title = column.footer().textContent;
 
                // Create input element
                let input = document.createElement('input');
                input.placeholder = title;
                column.footer().replaceChildren(input);
 
                // Event listener for user input
                input.addEventListener('keyup', () => {
                    if (column.search() !== this.value) {
                        column.search(input.value).draw();
                    }
                });
            });
        // add range filter for datation
        this.api()
            .columns('.range-filter')
            .every(function () {
                let column = this;
                
                // Create two input elements
                let inputFrom = document.createElement('input');
                let inputTo = document.createElement('input');
                inputFrom.placeholder = 'from';
                inputTo.placeholder = 'to';
                column.footer().replaceChildren(inputFrom, inputTo);
 
                // Event listener for user input
                inputFrom.addEventListener('keyup', () => {
                    // This is where I block - how to do the search on the attributes?
                    if (column.search() < this.value) {
                        column.search(inputFrom.value).draw();
                    }
                });
                inputTo.addEventListener('keyup', () => {
                    // This is where I block - how to do the search on the attributes?
                    if (column.search() > this.value) {
                        column.search(inputTo.value).draw();
                    }
                });
            });
    }
});

This question has an accepted answers - jump to answer

Answers

  • allanallan Posts: 65,720Questions: 1Answers: 10,930 Site admin

    Interesting one! What you'll need to do for this is use a function for the search term when you pass it to column().search(). That function will be passes the cell's row and column data index so you can do:

    column.search((term, data, row, column) => {
      let node = api.cell(row, column).node();
      let from = node.getAttribute('data-from');
      let to = node.getAttribute('data-to');
    
      // Perform logic check and then
      // return true / false;
    });
    

    I'd suggest doing let inputFromValue = inputFrom.value; (and for the to) in the event listener, to cache those values.

    As you mention, you will need logic checks for what to do is one of the two inputs isn't present. In such a case I'd just treat it as a single filter.

    It should be noted that this isn't going to be the most efficient since the data is being read from the DOM. Having the to/from values in the row's data object (i.e. a JSON object loaded from Ajax) would give better performance.

    Allan

  • heronalexheronalex Posts: 3Questions: 1Answers: 0

    Thanks @allan for the tip! Unfortunately my grasp of javascript and DataTables API is still a bit shaky, so I do not really understand how to put your advice in practice... (I have been mainly copy-pasting from examples).

    It is not clear to me how to use the function to pass the search term to column.search(), or where the javascript code that you provided should go with respect to the event listeners.

    I'm going to look into Ajax/JSON after I manage to make this first version work!

    I'm updating the relevant javascript with what I know is wrong code but should illustrate even better what I want to do :#

    // add range filter for datation
            this.api()
                .columns('.range-filter')
                .every(function () {
                    let column = this;
                    
                    // Create two input elements
                    let inputFrom = document.createElement('input');
                    let inputTo = document.createElement('input');
                    inputFrom.placeholder = 'from';
                    inputTo.placeholder = 'to';
                    column.footer().replaceChildren(inputFrom, inputTo);
    
                    // function to search "from"
                    column.search((term, data, row, column) => {
                        let node = api.cell(row, column).node();
                        let from = node.getAttribute('data-from');
                        let to = node.getAttribute('data-to');
     
                        // Return the item if
                        if (
                            // it has both unknown Terminus Post/Ante Quem
                            (isNaN(from) && isNaN(to)) ||
                            // it has an unknown TAQ and TPQ is equal/higher than the From input
                            // eg: item dated -332 to unknown should be found when searching items from -332, -300, 200 ... 
                            (inputFromValue <= from && isNaN(to)) ||
                            // it has a Terminus Ante Quem higher than the From input
                            // eg: item dated from -332 to 565 should be found for user searching items starting from 500
                            // (but probably not for user searching from 565...)
                            (inputFromValue < to) 
                        ) {
                            return true;
                        }
                        // else do not include the item in search results
                        return false;
                    });
    
                    // function to search "to"
                    column.search((term, data, row, column) => {
                        let node = api.cell(row, column).node();
                        let from = node.getAttribute('data-from');
                        let to = node.getAttribute('data-to');
     
                        // Return the item if
                        if (
                            // it has both unknown Terminus Post/Ante Quem
                            (isNaN(from) && isNaN(to)) ||
                            // it has an unknown TPQ and TAQ is equal/lower than the From input
                            // eg: item dated unknown to 565 should be found when searching items up to -300, 200, 565 
                            (isNaN(from) && inputToValue <= to) ||
                            // it has a Terminus Post Quem equal or lower than the To input
                            // eg: item dated from -332 to 565 should be found for user searching items up to -300
                            (from <= inputToValue) 
                        ) {
                            return true;
                        }
                        // else do not include the item in search results
                        return false;
                    });
     
                    // Event listener for user input
                    inputFrom.addEventListener('keyup', () => {
                        let inputFromValue = inputFrom.value;
                        // call a function to compare inputFromValue with data-to
                        // how to do that?
                    });
                    inputTo.addEventListener('keyup', () => {
                        let inputToValue = inputTo.value;
                        // call a function to compare inputToValue with data-from
                        // how to do that?
                    });
                });
    
  • allanallan Posts: 65,720Questions: 1Answers: 10,930 Site admin
    Answer ✓

    Something like this:

                let runSearch = () => {
                    column
                        .search((term, data, row, column) => {
                            let node = api.cell(row, column).node();
                            let from = node.getAttribute('data-from');
                            let to = node.getAttribute('data-to');
    
                            // Perform logic check
                            if (inputFromValue && !inputToValue) {
                                return from >= inputFromValue;
                            }
                            // TODO other logic checks
                            return true;
                        })
                        .draw();
                };
    
                // Event listener for user input
                inputFrom.addEventListener('keyup', () => {
                    // This is where I block - how to do the search on the attributes?
                    inputFromValue = inputFrom.value;
                    runSearch();
                });
                inputTo.addEventListener('keyup', () => {
                    // This is where I block - how to do the search on the attributes?
                    inputToValue = inputTo.value;
                    runSearch();
                });
    

    https://live.datatables.net/jamidedo/1/edit

    I'm not certain my logic check is exactly right for this use case, and I've left the other ones for you to do :).

    Allan

  • heronalexheronalex Posts: 3Questions: 1Answers: 0

    Thanks @allan, that's great :smiley: at least your example is working, even if we wtill need to convert strings to numbers!

    I still need to check the logic for the full data, I do not really get everything (for instance some items do not come back when I delete the search input...)

    I have one last question about this:

    When I separate the search in two different functions (from/to), the first filters the table and the second starts over from the whole dataset instead of the already filtered data.

    Is there a way to work from the already filtered data, instead of doing all the logic checks in a single function? I thought it would improve the speed since it can do at least one of the from/to search on a smaller dataset, but I did not manage to make it work.

  • allanallan Posts: 65,720Questions: 1Answers: 10,930 Site admin

    The search is cumulative for each draw, but each draw will start the search from the full set. It used to be that I tried to have a cumulative filter from draw to draw, but it was getting harder and harder to know when to invalidate it and with the introduction of functions, it became basically impossible.

    So what should happen on a draw is that the first filter will see the full data set, the second filter will see the result from the first, the third filter will see the result from the second, etc, etc.

    Allan

Sign In or Register to comment.