Pages

Monday, February 24, 2014

A Multi-Term jQuery Mobile Listview Filter

If you've done any development with jQuery Mobile you've likely used the Listview Widget. It's a handy little component that makes rendering list data a breeze. Even better, it has a built in search filter that you can enable by simply decorating your list element with data-filter="true"



The default behavior of the Listview Widget's filter is to match on a single term. The moment you add an additional space into your search string, look out! But, if you need multi-term search behavior in your filter, it's easy to add.

Attach to jQuery Mobile's 'mobileinit' event and assign a new function to the $.mobile.listview.prototype.options.filterCallback. Remember, this event must occur BEFORE jQuery Mobile has been loaded, so this script will need to load before the jQuery Mobile JS file.

$(document).on('mobileinit', function () {
    $.mobile.listview.prototype.options.filterCallback = function (currentItem, searchString) {

        var result = true;
        var resultCount = 0;
        var matches = $.trim(currentItem).split(/\s+/);
        var tokens = $.trim(searchString).split(/\s+/);
        var matchCount = matches.length;

        $.each(matches, function (index, match) {
            $.each(tokens, function (childIndex, token) {
                if (match.toLowerCase().indexOf(token) >= 0) {
                    resultCount++;
                }
            });
        });

        if (resultCount === matchCount) {
            result = false;
        }

        return result;
    };
});

The code splits out the incoming item and search string into a set of matches and tokens. If the text of the current item contains a token from the search string, we increment a result counter. After iterating through all of the matches, if the current items string match count equals the number of tokens in the search string, then we know we have a match on all terms.

I hope this helps!

7 comments:

  1. I found that I needed to compare the length of my tokens to the results matched, not the length of the total matchcounts (which can be quite large depending on possible search terms).

    Working for me:

    $(document).on('mobileinit', function () {
    console.log('JQM Fiter script hit');
    $.mobile.listview.prototype.options.filterCallback = function (currentItem, searchString) {
    //console.log(currentItem + ' : ' + searchString);
    var result = true;
    var resultCount = 0;
    var matches = $.trim(currentItem).split(/\s+/);
    var tokens = $.trim(searchString).split(/\s+/);
    var matchCount = matches.length;

    $.each(matches, function (index, match) {
    //console.log(match);
    $.each(tokens, function (childIndex, token) {
    //console.log(token);
    if (match.toLowerCase().indexOf(token) > -1) {
    resultCount++;
    }
    });
    });

    if (resultCount === tokens.length) {
    result = false;
    }

    return result;
    };
    });

    ReplyDelete
  2. Thanks for the comment John. I'll have to try this out!

    ReplyDelete
  3. I found that the one i posted before had an issue where multiple hits for a single search term could return incorrect results. I've updated the source, and put it in my header before the JQuery mobile.cs loads in - this applies itself to all filterable objects in the page:

    $(document).one("mobileinit", function () {
    $.mobile.filterable.prototype.options.filterCallback = function (index, searchValue) {
    $thisEl = this;
    //console.log(this.dataset.filtertext + ' : ' + searchValue);
    var result = true;
    var resultCount = 0;
    var matches = $.trim(this.dataset.filtertext).split(/\s+/);
    var tokens = $.trim(searchValue).split(/\s+/);
    var matchCount = matches.length;
    tokenCheck = new Array();
    $.each(tokens, function (childIndex, token) {
    tokenCheck.push({matched : false, toke : token});
    });
    //ma.push(matches);
    //to.push(tokens);
    $.each(matches, function (index2, match) {
    //console.log(match);
    $.each(tokenCheck, function (childIndex, token) {
    //console.log(token);
    if (match.toLowerCase().indexOf(token.toke.toLowerCase()) > -1 && token.matched == false) {
    resultCount++;
    token.matched = true;
    //console.log(resultCount + token.toke);
    }
    });
    });

    if (resultCount >= tokens.length || searchValue === '') {
    result = false;
    }
    //console.log(result);
    return result;
    };
    });

    ReplyDelete
  4. Oh and i declared the tokenCheck array right before this script (for checking/testing in dom):

    var tokenCheck = new Array();
    $(document).one("mobileinit", function () {.......

    ReplyDelete
  5. Great work John. When I have a minute I'm going to create a Fiddle that incorporates all the work that you've done. Thanks!

    ReplyDelete
  6. Can I convince one of you guys to post a Fiddle or a working example?

    ReplyDelete
    Replies
    1. Hey Mike, here you go: http://jsfiddle.net/everettcomstock/xwLdg0qm/2/

      Delete