Ag-Grid Custom Filtering

Hello @AnnMarieW

With the latest example I provided, does it look like I’m on the right track to get the YearFilter to work? I’m having trouble understanding how to make the RadioItems function as the example docs did.

Hi @robertf

Yes, you made a good start. I also got stuck on making the RadioItems function. This works slightly different from components in cell.

I think we need some guidance from the Dash AG Grid guru @jinnyzor

Hello @robertf,

There isnt currently a way to do this, but I have a PR pending that will fix it so that you can do something like this:

app.py

from dash import Dash, html
import dash_ag_grid as dag
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

rowData = df.to_dict('records')

columnDefs = [
    { 'field': 'age', 'filter': 'agNumberColumnFilter' },
    { 'field': 'country', 'minWidth': 150 },
    { 'field': 'year', 'filter': {'function': 'YearFilter'}},
    {
      'field': 'date',
      'minWidth': 130,
      'filter': 'agDateColumnFilter',
      'filterParams': {
        'comparator': {'function':'dateComparator'},
      },
    },
    { 'field': 'sport' },
    { 'field': 'gold', 'filter': 'agNumberColumnFilter' },
    { 'field': 'silver', 'filter': 'agNumberColumnFilter' },
    { 'field': 'bronze', 'filter': 'agNumberColumnFilter' },
    { 'field': 'total', 'filter': 'agNumberColumnFilter' },
]

defaultColDef = {
    'editable': True,
    'sortable': True,
    'flex': 1,
    'minWidth': 100,
    'filter': True,
    'resizable': True,
}


app.layout = html.Div(
    [
        html.H3("See the custom filter component in the Year column"),
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=rowData,
            defaultColDef=defaultColDef
        ),
    ]
)


if __name__ == "__main__":
    app.run(debug=True)

dashAgGridFunctions.js

This example was adapted from React Data Grid: Filter Component
The only differences are:

  • React.createElement instead of JSX
  • setProps, which all Dash components use to report user interactions, nstead of a plain js event handler
var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

const [useImperativeHandle, useState, useEffect, forwardRef, Component] = [React.useImperativeHandle, React.useState, React.useEffect, React.forwardRef, React.component]

dagfuncs.YearFilter = forwardRef((props, ref) => {
   const [year, setYear] = useState('All');

   useImperativeHandle(ref, () => {
       return {
           doesFilterPass(params) {
               return params.data.year >= 2010;
           },

           isFilterActive() {
               return year === '2010'
           },

           // this example isn't using getModel() and setModel(),
           // so safe to just leave these empty. don't do this in your code!!!
           getModel() {
           },

           setModel() {
           }
       }
   });

   useEffect(() => {
       props.filterChangedCallback()
   }, [year]);

    setProps = (props) => {
        if (props.value) {
            setYear(props.value)
        }
    }

    return React.createElement(
        window.dash_core_components.RadioItems,
        {
            options:[
                {'label': 'All', 'value': 'All'},
                {'label': 'Since 2010', 'value': '2010'},
            ],
            value: year,
            setProps
        }
        )
});

dagfuncs.dateComparator = function (filterLocalDateAtMidnight, cellValue) {

    const dateAsString = cellValue;
    const dateParts = dateAsString.split('/');
    const cellDate = new Date(
        Number(dateParts[2]),
        Number(dateParts[1]) - 1,
        Number(dateParts[0])
    );
    if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
        return 0;
    }
    if (cellDate < filterLocalDateAtMidnight) {
        return -1;
    }
    if (cellDate > filterLocalDateAtMidnight) {
        return 1;
    }

}
1 Like

Thanks @jinnyzor! I’ll test this out as soon as I can. Then I’ll attempt to expand upon it and get the PersonFilter to work.

1 Like

Hi @robertf

Please note that the PR has not been merged or released yet. This will likely happen sometime next week. :crossed_fingers:

This will be available on dash-ag-grid>=2.4.0

1 Like

Hi @robertf

:tada: dash-ag-grid-2.4.0 is now available!

The example that @jinnyzor posted above now works. It shows how to use a custom component in the header to filter the grid. This example uses dcc.RadioItems

As a bonus, it also demonstrates how to filter the Date column using a date filter comparator function in JavaScript. You can use this instead of the d3.timeParse. More info in the dash docs date filters section.

ag-grid-custom-function

dag-docs

3 Likes

Thanks @jinnyzor and @AnnMarieW ! I’ll be working with this again soon and will report back if I am having issues adapting this to my use case.

1 Like

Hello @jinnyzor

I took some time to apply what you showed towards creating the PersonFilter from the docs. I am also wanting to add an Apply and Remove/Clear button, since it currently is actively filtering as the person types. I have things mostly working, except I am not able to figure out how to delay applying the filter or how to clear it. I tried a few things (like trying to save as a global variable) but nothing works. Also, I am not able to clear the textbox after typing in it (the 1st character always stays.)

Below is what I have so far.

app.py

from dash import Dash, html
import dash_ag_grid as dag
import pandas as pd

app = Dash(__name__)


df = pd.read_json('https://www.ag-grid.com/example-assets/olympic-winners.json', convert_dates=False)

rowData = df.to_dict('records')

columnDefs = [
    { 'field': 'athlete', 'minWidth': 150, 'filter': {'function': 'PersonFilter' } },
    { 'field': 'age', 'filter': 'agNumberColumnFilter' },
    { 'field': 'country', 'minWidth': 150 },
    # { 'field': 'year', 'filter': {'function': 'YearFilter' }},
    {
      'field': 'date',
      'minWidth': 130,
      'filter': 'agDateColumnFilter',
      'filterParams': {
        'comparator': {'function':'dateComparator'},
      },
    },
    { 'field': 'sport' },
    { 'field': 'gold', 'filter': 'agNumberColumnFilter' },
    { 'field': 'silver', 'filter': 'agNumberColumnFilter' },
    { 'field': 'bronze', 'filter': 'agNumberColumnFilter' },
    { 'field': 'total', 'filter': 'agNumberColumnFilter' },
]

defaultColDef = {
    'editable': True,
    'sortable': True,
    'flex': 1,
    'minWidth': 100,
    'filter': True,
    'resizable': True,
}


app.layout = html.Div(
    [
        dag.AgGrid(
            id="filter-component-example",
            columnDefs=columnDefs,
            rowData=rowData,
            defaultColDef=defaultColDef
        ),
    ]
)


if __name__ == "__main__":
    app.run(host="127.0.0.1", debug=True)

dashAgGridFunctions.js

var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

const [useImperativeHandle, useRef, useState, useEffect, forwardRef, Component] = [React.useImperativeHandle, React.useRef, React.useState, React.useEffect, React.forwardRef, React.component]

dagfuncs.PersonFilter = forwardRef((props, ref) => {
    const [filterText, setFilterText] = useState(null);

  // expose AG Grid Filter Lifecycle callbacks
  useImperativeHandle(ref, () => {
    return {
      doesFilterPass(params) {
        const { api, colDef, column, columnApi, context } = props;
        const { node } = params;

        // make sure each word passes separately, ie search for firstname, lastname
        let passed = true;
        filterText
          .toLowerCase()
          .split(' ')
          .forEach((filterWord) => {
            const value = props.valueGetter({
              api,
              colDef,
              column,
              columnApi,
              context,
              data: node.data,
              getValue: (field) => node.data[field],
              node,
            });

            if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
              passed = false;
            }
          });

        return passed;
      },

      isFilterActive() {
        return filterText != null && filterText !== '';
      },

      getModel() {
        if (!this.isFilterActive()) {
          return null;
        }

        return { value: filterText };
      },

      setModel(model) {
        setFilterText(model == null ? null : model.value);
      },
    };
  });

  setProps = (props) => {
    if (props.value) {
        setFilterText(props.value)
    }
    };

  useEffect(() => {
    props.filterChangedCallback();
  }, [filterText]);

  function buttonOnApply(){
    console.log('applied');
  };

  function buttonOnRemove(){
    console.log('filter removed');
  };

  return React.createElement(
    'div',
    {},
    [
        React.createElement(window.dash_core_components.Input, {type:'text', placeholder:'enter text', value:filterText, setProps}),
        React.createElement('button', {onClick:buttonOnApply}, 'Apply'),
        React.createElement('button', {onClick:buttonOnRemove}, 'Remove Filter'),
    ]
  )

});

Hello @robertf,

You were close, try this:

dagfuncs.PersonFilter = forwardRef((props, ref) => {
    const [filterText, setFilterText] = useState(null);

  // expose AG Grid Filter Lifecycle callbacks
  useImperativeHandle(ref, () => {
    return {
      doesFilterPass(params) {
        const { api, colDef, column, columnApi, context } = props;
        const { node } = params;

        // make sure each word passes separately, ie search for firstname, lastname
        let passed = true;
        filterText
          .toLowerCase()
          .split(' ')
          .forEach((filterWord) => {
            const value = props.valueGetter({
              api,
              colDef,
              column,
              columnApi,
              context,
              data: node.data,
              getValue: (field) => node.data[field],
              node,
            });

            if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
              passed = false;
            }
          });

        return passed;
      },

      isFilterActive() {
        return filterText != null && filterText !== '';
      },

      getModel() {
        if (!this.isFilterActive()) {
          return null;
        }

        return { value: filterText };
      },

      setModel(model) {
        setFilterText(model == null ? null : model.value);
      },
    };
  });

  setProps = ({value}) => {
        setFilterText(value)
    };

  useEffect(() => {
    props.filterChangedCallback();
  }, [filterText]);

  function buttonOnApply(){
    console.log('applied');
  };

  function buttonOnRemove(){
    console.log('filter removed');
    setFilterText('')
  };

  return React.createElement(
    'div',
    {},
    [
        React.createElement(window.dash_core_components.Input, {type:'text', placeholder:'enter text', value:filterText, setProps, debounce: true}),
        React.createElement('button', {onClick:buttonOnApply}, 'Apply'),
        React.createElement('button', {onClick:buttonOnRemove}, 'Remove Filter'),
    ]
  )

});

This uses the input’s debounce prop to keep from rendering each time you type. Removing the filter button works.

Also, a slight modification to your setProps → setProps = ({value}) this means that we are only listening for the value object, and anytime this is updated, we would apply it, including blank strings (this registers false when testing for values).

1 Like

Thanks, that worked!

There is a small change I’d like to make. It looks like the filter applied whenever you click out of the filter box. I’d like to make the Apply button work for this instead. When clicking outside the box, I’d like for the text to go back to whatever the filterText was originally (like how the regular filtering handles it). Do you know of a way to accomplish this?

Try this one:

dagfuncs.PersonFilter = forwardRef((props, ref) => {
    const [filterText, setFilterText] = useState(null);

  // expose AG Grid Filter Lifecycle callbacks
  useImperativeHandle(ref, () => {
    return {
      doesFilterPass(params) {
        const { api, colDef, column, columnApi, context } = props;
        const { node } = params;

        // make sure each word passes separately, ie search for firstname, lastname
        let passed = true;
        filterText
          .toLowerCase()
          .split(' ')
          .forEach((filterWord) => {
            const value = props.valueGetter({
              api,
              colDef,
              column,
              columnApi,
              context,
              data: node.data,
              getValue: (field) => node.data[field],
              node,
            });

            if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
              passed = false;
            }
          });

        return passed;
      },

      isFilterActive() {
        return filterText != null && filterText !== '';
      },

      getModel() {
        if (!this.isFilterActive()) {
          return null;
        }

        return { value: filterText };
      },

      setModel(model) {
        setFilterText(model == null ? null : model.value);
      },

      afterGuiDetached() {
        if (filterValue != filterText) {
            var oldValue = filterText || ''
            setFilterText('unused string that will never be typed because it needs to render as new')
            setFilterText(oldValue)
        }
      }
    };
  });

  var filterValue = JSON.parse(JSON.stringify(filterText));

  setProps = ({value}) => {
        filterValue = value
    };

  useEffect(() => {
    props.filterChangedCallback();
  }, [filterText]);

  function buttonOnApply(){
    setFilterText(filterValue)
  };

  function buttonOnRemove(){
    setFilterText('')
  };

  return React.createElement(
    'div',
    {},
    [
        React.createElement(window.dash_core_components.Input, {type:'text', placeholder:'enter text', value:filterText, setProps, debounce: true}),
        React.createElement('button', {onClick:buttonOnApply}, 'Apply'),
        React.createElement('button', {onClick:buttonOnRemove}, 'Remove Filter'),
    ]
  )

});

Thanks again @jinnyzor !

My only issue now is that whenever I add some testing code to check when rows are processed (below), it looks like the filter is running an update whenever it clears out the text (since its running the setFilterText). I was trying to disconnect the filterText from the overall filtering text so that I can only run setFilterText when the Apply button is pressed. I think I need to make a Parent Component that will handle this interaction, but I’m unsure if thats the route to go, and if so, I can’t quite figure out how to wire things up correctly. Or maybe there is a way to save the text globally? Do you have any suggestions?

doesFilterPass(params) {
      console.log('doesFilterPass??');

The issue I was running into was the component wasnt rerendering unless the filterText changed.

Here is a version that might work:

dagfuncs.PersonFilter = forwardRef((props, ref) => {
    const [filterText, setFilterText] = useState(null);
    const [oldValue, setOldValue] = useState(null);

  // expose AG Grid Filter Lifecycle callbacks
  useImperativeHandle(ref, () => {
    return {
      doesFilterPass(params) {
        const { api, colDef, column, columnApi, context } = props;
        const { node } = params;

        // make sure each word passes separately, ie search for firstname, lastname
        let passed = true;
        filterText
          .toLowerCase()
          .split(' ')
          .forEach((filterWord) => {
            const value = props.valueGetter({
              api,
              colDef,
              column,
              columnApi,
              context,
              data: node.data,
              getValue: (field) => node.data[field],
              node,
            });

            if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
              passed = false;
            }
          });

        return passed;
      },

      isFilterActive() {
        return filterText != null && filterText !== '';
      },

      getModel() {
        if (!this.isFilterActive()) {
          return null;
        }

        return { value: filterText };
      },

      setModel(model) {
        setFilterText(model == null ? null : model.value);
      },

      afterGuiDetached() {
        if (filterValue != filterText) {
            setFilterText('unused string that will never be typed because it needs to render as new')
            setFilterText(oldValue || '')
        }
      }
    };
  });

  var filterValue = JSON.parse(JSON.stringify(filterText));

  setProps = ({value}) => {
        filterValue = value
    };

  useEffect(() => {
    if ('unused string that will never be typed because it needs to render as new' != filterText && (oldValue || '') != filterText) {
        setOldValue(filterText)
        props.filterChangedCallback();
    }
  }, [filterText]);

  function buttonOnApply(){
    setFilterText(filterValue)
  };

  function buttonOnRemove(){
    setFilterText('')
  };

  return React.createElement(
    'div',
    {},
    [
        React.createElement(window.dash_core_components.Input, {type:'text', placeholder:'enter text', value:filterText, setProps, debounce: true}),
        React.createElement('button', {onClick:buttonOnApply}, 'Apply'),
        React.createElement('button', {onClick:buttonOnRemove}, 'Remove Filter'),
    ]
  )

});
1 Like

Thanks again @jinnyzor !

I tested this out and it was able to do everything I was looking for. I’ll take some time to go through this code and learn why it works. Thanks again for all the help!

Overall it looks like the original docs can be reproduced with some enhancements. It would be good to have this be an official example for a new “Filter Component” section (under the Dash AG-Grid Components section) on the Plotly site. I can help contribute to this if you think its a good idea.

2 Likes

Hello @jinnyzor
I was testing out a few things and noticed that I am not able to take advantage agMultiColumnFilter (I have ag-grid enterprise). Whenever I try it out, I get this error “Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.”

I tried to place the component in the dashAgGridComponentFunctions.js file but that also didn’t work. Does dash allow a custom filter component to be used inside multiple filter components? I want to make sure its supported before I try too many things.

Below is the new column definition for the athlete field.

 { 'field': 'athlete', 'minWidth': 150, 
     'filter': 'agMultiColumnFilter',
     'filterParams':{
         'filters':[
             {
                'filter':{'function': 'PersonFilter' }
             },
             {
                'filter':'agTextColumnFilter'
             }
         ]
     }
    },

Filters is not connected to iterate through.

You can pass the whole filterParams a function I think and return exactly what you are looking for.

I’m sorry, I’m not sure what you mean exactly. The filters list does get iterated through if I provide default filter components. Its only when I add mine in that an error occurs.

Are you saying something like this? That filterParams can be a function?

{ 'field': 'athlete', 'minWidth': 150, 
     'filter': 'agMultiColumnFilter',
     'filterParams':{
         'function': [<return list of filter components>]
     }
    }

So far, moving the custom filter component to dashAgGridComponentFunctions.js works the “most” as in things render and clicks register, just no filtering is happening. Its saying I’m missing methods.

I would do something like this:

{ 'field': 'athlete', 'minWidth': 150, 
     'filter': 'agMultiColumnFilter',
     'filterParams':{
         'function': 'mycustomFilters(params)'
     }
    }

js file

PersonFilter = function (params) // definition

dagfuncs.mycustomFilters  = function (params) {
       return {  'filters':[
             {
                'filter': PersonFilter
             },
             {
                'filter':'agTextColumnFilter'
             }
         ]
}
     }
1 Like

This worked, thanks again!
I wouldn’t have expected this would be the way but I’m glad its possible. I’ll include this is the example I plan to make.

2 Likes

Hello @jinnyzor,

I tried to build a custom filter similar as above. Sadly, I don’t get it completely to work.
I have an infinite row model and hope to pass the filter parameter to the filterModel. When I use my custom filter, I can see that a new request is triggered, but the filter is not passed. In my dev tools I found that the methods “setModel()” and “getModel()” are missing AG Grid: Framework component is missing the method getModel()

I am not sure how to fix it and hope you might be able to help.

My filter looks like the following:

const [useImperativeHandle, useState, useEffect, forwardRef, Component] = [React.useImperativeHandle, React.useState, React.useEffect, React.forwardRef, React.component];


dagcomponentfuncs.SelectFilter = forwardRef((props, ref) => {
    const [select, setSelect] = useState('all');
    

    // Select options are the options passed in params
    var selectOptions = props.select_options


    // expose AG Grid Filter Lifecycle callbacks
    useImperativeHandle(ref, () => {
        console.log('test');
        return {
            doesFilterPass(params) {
                console.log('filter passed');
                return true;
            },

            isFilterActive() {
                console.log('filter is active');
                return true;
            },

            getModel() {
                console.log("get model");
                if (!this.isFilterActive()) {
                  return null;
                }
                console.log(select);
                return { value: select };
            },

            setModel(model) {
                console.log('model')
                console.log(model);
            },
        };
    });

    useEffect(() => {
        props.filterChangedCallback();
    }, [select]);

    setProps = ({value}) => {
        // Click event on change we set the filterValue
        console.log(value);
        filterValue = value;
        setSelect(props.value);
    };

    // Create the layout
    var radio_items = [];

    // Iterate over the items and make radio buttons
    for (item in selectOptions) {
        radio_items.push(React.createElement(window.dash_mantine_components.Radio, selectOptions[item]));
            
    };
    
    // make the filter
    var RadioFilter = React.createElement(
        'div',
        {},
        [
            React.createElement(window.dash_mantine_components.RadioGroup, { orientation: 'vertical', setProps, debounce: true}, radio_items),
        ]
    );

    return RadioFilter;

});