jbox-web / ajax-datatables-rails

A wrapper around DataTable's ajax methods that allow synchronization with server-side pagination in a Rails app
MIT License
585 stars 228 forks source link

Range filter? #301

Closed ohaddahan closed 5 years ago

ohaddahan commented 6 years ago

I see there is a date range filter, but is it possible to filter by min & max? From my understanding, only one condition per column is supported.

ohaddahan commented 6 years ago

Ok, couple of updates @n-rodriguez .

  1. I updated my version, was using the older version hence some of the docs were of sync for me :)

  2. I noticed that Arel::Attributes::Attribute:in checks if the input argument is a Range (also see the deprecation warning & that the current default behavior, :in evaluates as SQL in which I assume very few people actually need, between seems like the common need)

  3. I managed to get it to work, it's ugly and definitely shouldn't be the official way, but will do as a proof of concept :)

I added the following formatter:

cond: :in , formatter: filter_in_range

  def filter_in_range
    lambda do |input|
      def to_float(string)
        Float(string)
      rescue ArgumentError
        string
      end

      a = input.split(',')
      case a.size
      when 2
        Range.new(to_float(a[0]), to_float(a[1]))
      when 1
        Range.new(to_float(input),999999999);
      else
        input
      end
    end
  end

And did the following monkey patching:

module AjaxDatatablesRails
  module Datatable
    class Column
      module Search
        private
        def out_of_range?(search_value)
          if search_value.is_a?(Range)
            return true if search_value.first < SMALLEST_PQ_INTEGER
            return true if search_value.last  > LARGEST_PQ_INTEGER
          else
            Float(search_value) > LARGEST_PQ_INTEGER || Float(search_value) < SMALLEST_PQ_INTEGER
          end
        end
        def integer?(string)
          true if string.is_a?(Range) || Float(string)
        rescue ArgumentError
          false
        end
      end
    end
  end
end

On the frontend , I'm currently manually inserting 9.1,15.4 but I'll change it and have two separate fake inputs and an event listener to format it into the real input field.

ohaddahan commented 6 years ago

For completeness the JS part I added :

This function is needed to prevent sending multiple request while the user is still typing. Due to the asynchronous nature and race condition, the user might see the wrong results. (for example typing 100 but actually seeing results for 10)

var delayExec = (function(){
  var timer = 0;
  return function(callback, ms){
    clearTimeout(timer);
    timer = setTimeout(callback, ms);
  };
})();

Sending the updated and formatted range to the DataTable filter.

function updateAjaxFilters(from, to, input) {
  var spinner = document.getElementById('table_processing');
  if (spinner !== null) {
    spinner.setAttribute('status','loading');
    spinner.innerHTML = '<i class="fa fa-spinner fa-spin fa-3x fa-fw"></i><h3>Loading...</h3>';
  };
  var fromVal = parseFloat(from.value);
  var toVal   = parseFloat(to.value);
  var range   = '';
  if (isNaN(toVal) && isNaN(fromVal)) {
    range = '';
  } else if (isNaN(toVal) && !isNaN(fromVal)) {
    range = fromVal;
  } else if (!isNaN(toVal) && isNaN(fromVal)) {
    range = 0 + "," + toVal;
  } else if (!isNaN(toVal) && !isNaN(fromVal)) {
    range = fromVal + "," + toVal;
  }
  input.value = range;
  var prevInput = input.getAttribute('prev-val');
  if (prevInput !== range) {
    var event = new Event('change');
    input.dispatchEvent(event);
    input.setAttribute('prev-val',range);
  }
}

Adding the event lister on all items the filter pair.


function addEvenListenerForAjaxTable(fromId, toId, inputId) {
  var from  = document.getElementById(fromId);
  var to    = document.getElementById(toId);
  var input = document.getElementById(inputId);
  if (from === null || to === null || input === null) {
    console.log("WARN ::: addEvenListenerForAjaxTable : fromId = " + fromId + " , toId =  " + toId + " inputId = " + inputId);
    return;
  }
  [from,to].forEach( function(element) {
    element.addEventListener('keyup',function(event) {
      delayExec(function(){
        updateAjaxFilters(from, to, input);
      }, 500 );
    });
  });
}

And I add this listener to each column:

['column_name_1','column_name_2', ........].forEach( function(name) {
          addEvenListenerForAjaxTable(name + '_filter_min', name + '_filter_max', name + '_filter');
      });

The column_name_filter input is the one ajax-datatables-rails tracks (I made it invisible). While column_name_filter_min/max are user input fields.

And :


    @view_columns ||= {
.....
.....
column_name:  { source: 'ModelName.column_name' , cond: :in , formatter: filter_in_range },
....
....
n-rodriguez commented 5 years ago

Hi! Thanks for your feedback!

I see there is a date range filter, but is it possible to filter by min & max?

Yes but actually most of the job is done by https://github.com/vedmack/yadcf.

It seems that you start rewriting yadcf ^^

When using yadcf, it automagically creates 2 input field for ranges : the right handed one is the min and the left handed one is the max.

When setting values in the inputs fields, yadcf automagically sends both value in the same colum with a field delimiter : <min>-yadfc-delimiter-<max>

On the server side you only manage 1 column with one 1 value that you need to split to do the correct job but that's all :)

ohaddahan commented 5 years ago

Didn't know yadcf does that, also didn't want to integrate another gem. If you delegate it to them, it's fine by me. But since it's a rather common need, I think a little more documentation about it and maybe an example will help a lot.

n-rodriguez commented 5 years ago

Didn't know yadcf does that, also didn't want to integrate another gem.

yadcf is not really a gem but a JS lib. It's a plugin for JQuery Datatables that enhances a lot datatables.

But since it's a rather common need, I think a little more documentation about it and maybe an example will help a lot.

Working on it.

ohaddahan commented 5 years ago

You can also use the code I pasted here as a work around if someone wants to use. I guess sometimes people will prefer a couple of methods instead of a adding a full library they have little visibility into.

tom-brown commented 3 years ago

I got a range filter to work like this:

in the datatable view I created a table above the datatable to input the range variables, then add some javascript to reload the datatable on change:

<table>
                <tbody><tr>
                    <td>Minimum CWD:</td>
                    <td><input type="text" id="minCWD" name="minCWD"></td>
                </tr>
                <tr>
                    <td>Maximum CWD:</td>
                    <td><input type="text" id="maxCWD" name="maxCWD"></td>
                </tr>
            </tbody></table>
<script>
$(document).ready(function () {             
            // other options
            var table = $('#example').DataTable()

            $("#minCWD").change(function () {
              table.ajax.reload();
            });
            $("#maxCWD").change(function() {
              table.ajax.reload();
            });
        });
</script>

then to add the filter variables to the ajax call (in the coffee file):

ajax: {
      url: $('#example').data('source'),
      beforeSend: (xhr) => xhr.setRequestHeader('Content-Type', 'application/json'),
      data: (d) ->
        $.extend {}, d, 'minCWD': $('#minCWD').val(),
        $.extend {}, d, 'maxCWD': $('#maxCWD').val()
    }
// note: the beforeSend may not be required

then add a filter in the model_datatable.rb:

def get_raw_records
    #YOUR TYPICAL SELECTION...note: I'm using AREL and joining schools with pstats
    #now filter by your max min variables
    if params['minCWD'].present?
      schools = schools.where(pstats[:cwd_percent].gteq(params['minCWD']))
    end
    if params['maxCWD'].present?
      schools = schools.where(pstats[:cwd_percent].lteq(params['maxCWD']))
    end
    return schools
  end

My controller looks like this:

respond_to do |format|
      format.html
      format.json { render json: ExampleDatatable.new(params, view_context: view_context) }
end

working example here: https://schoolsparrow.com/arizona/schools