telerik / kendo-react

Issue tracker - KendoReact http://www.telerik.com/kendo-react-ui/
https://kendo-react-teal.vercel.app
Other
212 stars 39 forks source link

Make Grid Filter Cells more customizable #8

Closed Xizario closed 6 years ago

Xizario commented 6 years ago

Currently there are only 5 options for the filter cells: text, boolean, numeric, date and disabled filtering for the column.

The user should be able to customize the filter cells similar to how he can define custom Cells for the data rows.

Possible solutions:

adrianbarbe commented 6 years ago

Сustom filter component for inside the filter cell would be nice option.

andreastoermer commented 6 years ago

Please at the custom filter component, it would help many user who want to create something that is not Standard.

jannhofman commented 6 years ago

in my mind the custom component for the filter cell itself is the most flexible option.

Xizario commented 6 years ago

A custom component to the cell would have such props as interface when the grid instantiate it.

interface FilterCellProps {
    field: any;
    filterType: 'text' | 'numeric' | 'boolean' | 'date';

    value: any;
    operator: string;

    onChange: (event: {
        value: any,
        operator: string,
        syntheticEvent: React.SyntheticEvent<any>
    }) => void;
}

Then the user will be able to create class like

class MyFilterCell extends React.Component<FilterCellProps, {}>{ /* custom logic here */}

Add pass it as custom cell for his columns:

<Column field="someField" filterCell={MyFilterCell} />

The pros:

The cons:

To avoid the cons, I am thinking of adding additional render prop to the Grid, that will be called for each filterCell that is about to be rendered. And this gives some more options for customization:

 <Grid
    ...
    filterCellRender={(cell: JSX.Element | null, props: FilterCellProps) => {       
        // return the original cell as it would be rendered without modification.
        return cell;
        // or clone and modify
        return cell && React.cloneElement(
            cell,
            {
                ...cellEditEvent,
                someAdditionalProps
            },
            cell.props.children
            //or some other children
        );
        // or change it entierly
        return (<EntierlySomethingDifferent {...props}/>);
    }}
>
jannhofman commented 6 years ago

Ok, now I got yout point. Because we dont use automatically generated columns i didnt see this before. For me, implementing the "fully working cell" ist not that big of a deal.

I guess the FilterCellProps in the second snippet are the same as listed above? In that case i dont have any access to the column itself to make a decision what kind of filter i need to render. The datatype alone is not sufficient.

To stay consistent in the column definition defining the filterCell in the same way as the customCell would be the best. If you want maximum fleixibility for all upcoming scenarions you could add the filterCellRender option on the grid and each column. The one on the colum would override the other.

Xizario commented 6 years ago

We plan to add both ways of customization. So you could use one of them, or both of them at the same time.

First if there is filterCell defined for the column, it will be instantiated. This is the MyFilterCellA in the example below.

If you have set filterCellRender, it will be passed through the props of the grid to the columns. And they could either use it, or ignore it, see MyFilterCellB.

If filterCellRender is set, it will be called for our built-in filters.

class MyFilterCellA extends GridFilterCell {
    render() {
        return (
            <div>
                my filter cell <b>A</b>
                <input
                    value={'VAL: ' + this.props.value}                    
                />
            </div>
        );
    }
}

class MyFilterCellB extends GridFilterCell {
    render() {
        const myRendering = (
            <div className="k-filtercell">
                My filter cell <b>B</b>
                <DropDownList />
            </div>
        );

        if (this.props.render) {
            return this.props.render(myRendering, this.props);
        }
        return myRendering;
    }
}

function cellRenderer(cell: any, props: GridFilterCellProps) {
    if (props.field === 'UnitsInStock') {
        // customize by field
        return cell;
    }

    const children = cell.props.children;
    return React.cloneElement(
        cell,
        {
            ...cell.props
        },
        [
            cell.props.children,
            (<div key={0}><input value={'OP:' + props.operator} /></div>)
        ]
    );
}
<Grid
    data={this.state.data}
    filterable={true}
    filter={this.state.filter}
    filterChange={this.filterChange}

    filterCellRender={cellRenderer}
>
    <Column field="ProductName" title="A" filterCell={MyFilterCellA} />
    <Column field="ProductName" title="B" filterCell={MyFilterCellB} />
    <Column field="ProductName" title="built-in cell" />
    <Column width="200px" field="UnitsInStock" title="clear built-in" />
</Grid>

image

And this is without the filterCellRenderer

    <Grid
        data={this.state.data}
        filterable={true}
        filter={this.state.filter}
        filterChange={this.filterChange}
    >
        <Column field="ProductName" title="A" filterCell={MyFilterCellA} />
        <Column field="ProductName" title="B" filterCell={MyFilterCellB} />
        <Column field="ProductName" title="built-in cell" />
        <Column width="200px" field="UnitsInStock" title="clear built-in" />
    </Grid>

image

jannhofman commented 6 years ago

This looks fine to me. :) But i have a question about the current interface. I have added a screenshot of our version, based on the jquery library. There you can see exactly the filter that i need to build. It is based on your multi-checkbox-filter with some added sugar. We use a configured delimiter to split the values if needed. (ignore the fact that its based on the headerrow and not the filterrow)

unbenannt

To make this work we need a CompositeFilterDescripter - same as in the GridFilterChangeEvent & GridFilterRowProps - for the initial value and the onChangeEvent that is limited to one field. The second part i am unsure about: how can i access the datasource to get the distinct values of my current resultset?

Xizario commented 6 years ago

@jannhofman, I would like to keep the filterCellProps as little as possible, passing all grid data to them, and giving them opportunity to return complex filter will break some separation of concern and encapsulation principles.

However, on application level, you could pass your data to your custom cells, by using a higher-order component.

<Column field="ProductName" filterCell={filterCellWithData(products, this.state.data)} />
function filterCellWithData(data: any[], currentData: any[]) {
    return class MyFilterCellB extends GridFilterCell {
        render() {
            const modifiedData = currentData.slice();
            modifiedData.push({ ProductName: '' });
            return (
                <div >
                    <DropDownList
                        textField="ProductName"
                        defaultValue="ProductName"
                        data={data}
                        onChange={(e) => {
                            this.props.onChange({
                                value: e.target.value.ProductName,
                                operator: 'eq',
                                syntheticEvent: e.syntheticEvent
                            });
                        }}
                    />
                    <DropDownList
                        textField="ProductName"
                        defaultValue="ProductName"
                        data={modifiedData}
                        onChange={(e) => {
                            this.props.onChange({
                                value: e.target.value.ProductName,
                                operator: 'contains',
                                syntheticEvent: e.syntheticEvent
                            });
                        }}
                    />
                </div>
            );
        }
    };
}

I have just placed two DropDown-s into the filter menu to give you and example, one is bound to all data, and the other only to the filtered(same as the grid) with additional empty item, see it in action.

And of course instead of the DropDown you could place listbox/multiselect and/or build complex descriptor and return it using callback passed in the filterCellWithData.

jannhofman commented 6 years ago

To be honest, i am new to react, never typed a line of code so far. Some of my develepoers are laughing right now - about me not knowing this pattern. :) You are right, with a HOC we can solve our needs. Thx for yout patience.

Xizario commented 6 years ago

In the latest dev version @progress/kendo-react-grid@0.5.0-dev.201804041130 we have added filterCellRender property of the grid as well as filterCell per each Column

An example of how to use the cellRender prop of the grid in general can be seen here: https://www.telerik.com/kendo-react-ui-develop/components/grid/editing/editing-in-cell/ Same approach is applicable for the filterCellRender as well for modifying the currently built-in cells.

An example with entirely custom filterCell per column can be seen here: https://www.telerik.com/kendo-react-ui-develop/components/grid/data-operations/filtering/#toc-custom-filter-cell

If this is resolves the issue, it will be closed when we publish next official version, so give it a try using the development builds.

Xizario commented 6 years ago

Available in v0.5.0

ZiedHf commented 6 years ago

I suggest to add an option to make the filter look like this one (from the kendo ui JQuery). An icon beside the title : image

aymendhaya commented 6 years ago

Agree with @ZiedHf , it will be awesome if we have for example filterDropDown same as filterCell, as per Grid Row/Cell editing, you are providing different approachs for editing (inline, modal, custom forms ..etc). The same thing for filters will be cool

andreastoermer commented 6 years ago

@ZiedHf Wonderful idea, something i miss in the current react version too, but i think that create a new issue is a better solution, because this one is closed.