Search code examples
jquerydjangoajaxtablesorter

Tablesorter's filter_selectSource removes options not available for current page with server-side pagination and select options preparation


I'm preparing arrays with options for Tablesorter's filter_selectSource in Python/Django. Because initially, it worked very strange for me when I switched to server-side filtering. The dropdown options did show up only when I've type one symbol in one of the 'search' filters and then every time it showed select options available one step before. And then I've decided to try and make most of the work on server-side

players.views

def skaters_averages_json(request, page, sort_col, filt_col, rookie_filt):

    start = utils.PAGE_SIZE_2*(page - 1)
    end = start + utils.PAGE_SIZE_2
    skaters = Skater.objects.select_related('team')

    filtering = utils.filter_columns(filt_col)
    if filtering:
        skaters = utils.apply_filters(skaters, filtering)

    if utils.rookie_filter(rookie_filt):
        skaters = skaters.filter(rookie=True)

    sorting = utils.sorting_columns(sort_col)
    one_page_slice = utils.sort_table(sorting, skaters)[start:end]

    skaters_json = json.loads(serializers.serialize('json', one_page_slice,
                                                    use_natural_foreign_keys=True))
    domain = request.get_host()
    total_rows = skaters.count()
    data = utils.process_json(domain, skaters_json, total_rows, page)

    data['filter_select'] = {
        **utils.filter_select_opts(skaters, 3, 'position_abbr'),
        **utils.filter_select_opts(skaters, 4, 'team__abbr'),
        **utils.filter_select_opts(skaters, 5, 'nation_abbr'),
    }

    return JsonResponse(data, safe=False)

players.utils

def filter_select_opts(skaters_query, col_number, field_name):
    uniques = list(skaters_query.values_list(field_name, flat=True).distinct())
    return {col_number: sorted(uniques, key=lambda x: (x is None, x))}

So my JSONResponse looks like this.

Page 1

{
  "total": 41,
  "rows": [
    [row 1],
    ...
    [row 25]
  ],

  "filter_select": {
    "3": [
      "C",
      "D",
      "LW",
      "RW"
    ],
    "4": [
      "ANA",
      "BOS",
      "BUF",
      "CAR",
      "CBJ",
      "CGY",
      "CHI",
      "COL",
      "DAL",
      "EDM",
      "FLA",
      "MIN",
      "MTL",
      "NJD",
      "NSH",
      "NYI",
      "PHI",
      "PIT",
      "SJS",
      "TOR",
      "VAN",
      "VGK",
      "WPG"
    ],
    "5": [
      "FIN"
    ]
  }
}

Page 2

{
  "total": 41,
  "rows": [
    [row 26],
    ...
    [row 41]
  ],

  "filter_select": {
  "3": [
    "C",
    "D",
    "LW",
    "RW"
  ],
  "4": [
    "ANA",
    "BOS",
    "BUF",
    "CAR",
    "CBJ",
    "CGY",
    "CHI",
    "COL",
    "DAL",
    "EDM",
    "FLA",
    "MIN",
    "MTL",
    "NJD",
    "NSH",
    "NYI",
    "PHI",
    "PIT",
    "SJS",
    "TOR",
    "VAN",
    "VGK",
    "WPG"
  ],
  "5": [
    "FIN"
  ]
}
}

The second page of the applied filter's response shows the same values for filter_select But when I look at the same pages after it is processed by filter_selectSource it shows only options available on the currently visible page. Is there a way to override this behavior?

Read the docs for filter_selectSource and getOptions. Couldn't find what I want.

external.js

let selectOptions;

$("#tab1")
  .tablesorter({
      headers: {
          0: {sorter: false, filter: false},
          1: {filter: false},
          2: {filter: false},
      },

      widgets: ['filter'],
      widgetOptions : {
      filter_selectSource: selectOptions,
      filter_reset: '.reset',
      filter_external : '.select',
  }
})

.tablesorterPager({
    container: $(".pager-s"),
    size: 25,
    output: '{page} / {totalPages}',
    savePages: false,
    fixedHeight: false,
    ajaxUrl: 'http://127.0.0.1:8000/skaters_averages_json/{page+1}/{sort:col}/{filter:fcol}',
    customAjaxUrl: function(table, url) {
    if ($("#tab1").data('filter_value')) {
          return url += '/rookie_filter=' + $("#tab1").data('filter_value');
    };
        return url += '/rookie_filter=';
    },

    ajaxObject: {
        success: function(data) {
          selectOptions = data['filter_select'];
          $("#tab1").trigger("updateAll");
          console.log(selectOptions);
        },
        dataType: 'json',
        type: 'GET'
      },
    });

$('.rookie-filter').on('change', function(){
    let checked = $(this).is(":checked");
    $("#tab1").data('filter_value', checked).trigger('pagerUpdate', 1);
    if (checked) {
      $(this).attr('title', 'Show all players')
    } else {
      $(this).attr('title', 'Show rookies only')
    };
});

$('.reset').on('click', function(){
  $('.rookie-filter').prop('checked', false);
  $("#tab1").data('filter_value', $(this).is(":checked")).trigger('pagerUpdate');
});

$('table').bind("sortEnd", function(){
    $(this).trigger('pageSet', 1);
  });

I'm also logging selectOptions in my external .js file to make sure I'm getting what's expected.

UPDATE. Have been trying to make it work with buildSelect, as @Mottie suggested in the comment. As it turned out, filter_selectSource actualldidn't accept my variable infilter_selectSource: selectOptions, the only line that makes a difference is$("#tab1").trigger("updateAll")inajaxObject`. Tablesorter then automatically shows the options available on the current page.

So, now I'm struggling with triggering updates for a filter row. I'm able to load expected options for one column at a time, but the table doesn't work because of the too much recursion error. $('body').trigger(filter_event) in every filter_selectSource causing enormous amount of updating a table. But without it options for dropdown menus are empty. I've tried to bind this update for different events. Like filterInit. Didn't work for me. Take a look at my updated script below. Any suggestions of how to make it update a filter row without repeating it multiple times?

$("#tab1").data('statType', 'tot');
let pos = [];
let team = [];
let nation = [];
let filter_event = jQuery.Event("load_filter_select");

$("#tab1")
  .tablesorter({
      headers: {
          0: {sorter: false, filter: false},
          1: {filter: false},
          2: {filter: false},
      },

      widgets: ['filter'],
      widgetOptions : {

        filter_selectSource: {
          3 : function(table, column, onlyAvail) {
            result = pos;
            $.tablesorter.filter.buildSelect(table, column, result, true);
            $('body').trigger(event);
          },

          4 : function(table, column) {
            result = team;
            $.tablesorter.filter.buildSelect(table, column, result, true);
            $('body').trigger(filter_event);
          },

          5 : function(table, column) {
            result = nation;
            $.tablesorter.filter.buildSelect(table, column, result, true);
            $('body').trigger(filter_event);
          },
        },

      filter_reset: '.reset',
      filter_external : '.select',
  }
})

.tablesorterPager({
    container: $(".pager-s"),
    size: 25,
    output: '{page} / {totalPages}',
    savePages: false,
    fixedHeight: false,
    ajaxUrl: 'http://127.0.0.1:8000/skaters_averages_json?/{page+1}/{sort:col}/{filter:fcol}',
    customAjaxUrl: function(table, url) {
      urlParts = url.split('?');
      url = urlParts[0] + `/${$("#tab1").data('statType')}` + urlParts[1];

      if ($("#tab1").data('filter_value')) {
            return url += '/rookie_filter=' + $("#tab1").data('filter_value');
      };
          return url += '/rookie_filter=';
    },

    ajaxObject: {
        success: function(data) {
          pos = data['filter_select'][3];
          team = data['filter_select'][4];
          nation = data['filter_select'][5];

          $('body').trigger(filter_event);

        },
        dataType: 'json',
        type: 'GET'
      },
    });

$('body').on('load_filter_select', function(){
    $("#tab1").trigger('update');
});

Solution

  • With the great help of Mottie I've been able to solve the problem.

    I shouldn't have to actually trigger any updates. Just load the options from AJAX callback using buildSelect function. So, the full script that makes select filters work properly looks like this:

    $("#tab1").data('statType', 'tot');
    
    $("#tab1")
      .tablesorter({
          widgets: ['filter'],
          widgetOptions: {
              filter_selectSource : {
                 ".filter-select" : function() { return null; }
              },
    
          }
    })
    
    .tablesorterPager({
        container: $(".pager-s"),
        size: 25,
        output: '{page} / {totalPages}',
        savePages: false,
        fixedHeight: false,
        ajaxUrl: 'http://127.0.0.1:8000/ajax_players?/{page+1}/{sort:col}/{filter:fcol}',
        customAjaxUrl: function(table, url) {
          urlParts = url.split('?');
          url = urlParts[0] + `/${$("#tab1").data('statType')}` + urlParts[1];
    
          if ($("#tab1").data('filter_value')) {
                return url += '/rookie_filter=' + $("#tab1").data('filter_value');
          };
              return url += '/rookie_filter=';
        },
    
        ajaxObject: {
            success: function(data) {
              table = $("#tab1");
              options = data['filter_select'];
    
              for (let column = 3; column <= 5; column++) {
                  $.tablesorter.filter.buildSelect(table, column, options[column], true);
              };
            },
            dataType: 'json',
            type: 'GET'
          },
        });
    

    UPDATE. I've actually just figured out why this solution was not working as expected in some cases. I forgot to add filter_selectSource : { ".filter-select" : function() { return null; } }, to the widget-options. Without the function returning null filter_selectSource was trying to override the options made by buildSelect. I've edited the code.