Learn to customize JavaScript grid in less than 10 minutes

  |   Javascript

Most of the functionality of our JavaScript grid is already component based. You can easily extend the default functionality by creating your custom components and integrating them into the grid.

Implement custom filtering logic, cell editor and renderer

Everything is a customizable component in JavaScript grid

In the previous article on how to get started with Javascript grid we’ve integrated ag-Grid into a sample application. If you haven’t worked through the tutorial and don’t have a sample application now, you can download it from this github repository.

The grid used in the application rendered the following data on the screen:

In this tutorial, we’ll provide more options for a user to work with the data in the “Price” column. We’re about to learn how to customize the grid with custom filters, cell renderers and cell editors. And we’ll have it all implemented and integrated in less than 10 minutes.

Custom cell renderer

First we’re going to implement a custom cell renderer to format numbers so that they are shown according to a user’s locale. Here’s how the numbers are displayed on my computer:

Custom cell editor

Since the “Price” field is numeric, it can’t contain non numeric characters. By default, when a cell is editable, users can type in any character. So we’ll implement a custom cell editor to restrict the input to numbers:

Custom filter

And we’ll finish this tutorial by implementing a custom filter. Our new filter will provide UI to type in price range and filter out cars that fall out of that range:

So let’s see how we can do that.

You can download the sample that we’ll be building in this article from github repository, in the customization-demo folder.

Customization through components

Our JavaScript grid is customized by implementing JavaScript components. A component is a simple a JavaScript class that implements methods for communication between ag-Grid and the component. For example, a cell editor should implement the getValue method that ag-Grid uses to obtain the value from our component and update the data. A custom column filter should implement the doesFilterPass method that processes values and so on. These required methods are defined by interfaces described in the docs.

The two methods that most components need to implement are init and getGui. The former is used to receive parameters from ag-Grid, and the latter is called by ag-Grid to get the view to renderer.


Custom cell renderer

The job of the grid is to lay out the cells. By default the grid will create the cell values using simple text. If you want more complex HTML inside the cells you need to implement cell renderers. We want to format our number according to a user’s locale so we need a custom component. The component will take advantage of the built-in function toLocaleString to format values.

Define custom component

The first task is to implement a component class. We need to define the init method that is used to receive the value of a cell through parameters. To show the formatted value we’ll use a simple span element. So, create a new file number-formatter.js and put the code for our component inside:

class NumberCellFormatter {
    init(params) {

        const text = params.value.toLocaleString(undefined, {style: 'currency', currency: 'EUR'});

        this.eGui = document.createElement('span');
        this.eGui.innerHTML = text;
    }

    getGui() {
        return this.eGui;
    }
}

It’s a simple component with just two methods init and getGui that returns a span element containing formatted text.

Register the component

Now that we have our component, we need to tell ag-Grid about it. All custom components should be listed in the components configuration option:


    const gridOptions = {
        columnDefs: columnDefs,
        components: {
            /* custom cell renderer component */
            numberCellFormatter: NumberCellFormatter,
        }
    };

Specify the renderer for the column

We’re almost done. The only thing that’s left is to specify our component as a cell renderer for the “Price” column. We can do that in the column definition:

    const columnDefs = [
        {headerName: 'Make', field: 'make'},
        {headerName: 'Model', field: 'model'},
        {
            headerName: 'Price',
            field: 'price',
            editable: true,

            /* custom cell renderer */
            cellRenderer: 'numberCellFormatter'
        }
    ];

Now if you run the application you should see the price formatted:


Custom cell editor

Our JavaScript datagrid provides rich editing capabilities. It resembles a spreadsheet allowing you to edit data inline. Just press F2 or double-click on a cell and ag-Grid activates the edit mode. You don’t need to provide a custom editor for simple string editing. But when we need to implement a custom logic, like restricting input to numbers, we need to create our own cell editor.

Enabling editing

Before we start working on a custom component that will act as an editor, we need to enable editing for the Price column:

    const columnDefs = [
        {headerName: 'Make', field: 'make'},
        {headerName: 'Model', field: 'model'},
        {
            headerName: 'Price',
            field: 'price',

            /* enable editing */
            editable: true,
        }
    ];

Define custom component

Similarly to a custom cell renderer, we need to define a component to act as a cell editor. Let’s start with the basic implementation that will just render an input element. ag-Grid will show the input when a user activates the edit mode. To get the value from the grid we’ll use init method. Here’s the implementation:

class NumericCellEditor {
    init(params) {
        this.textInput = document.createElement('input');
        this.textInput.value = params.value;
    }

    getGui() {
        return this.textInput;
    }
}

Now we need to implement some required methods.

Once editing is finished by pressing Enter or removing the focus from the input, ag-Grid needs to get a value from our editor. To do that, it calls the getValue method on our component that should return the result of the editing:

class NumericCellEditor {
    init(params) {
        this.textInput = document.createElement('input');
        this.textInput.value = params.value;
    }

    getValue() {
        return Number(this.textInput.value);
    };
    
    ...
}

Now we’re ready to implement the functionality to filter out non numeric characters. To do that, we need to add an event listener to the input and check each item typed in by the user. We also need to add the keyDown listener to prevent losing focus on the input when the user presses navigation keys. Here’s how we do it:

class NumericCellEditor {
    init(params) {
        this.textInput = document.createElement('input');
        this.textInput.value = params.value;

        this.textInput.addEventListener('keypress', (event) => {
            if (!isNumeric(event)) {
                event.preventDefault();
            }

            function isNumeric(event) {
                return /\d/.test(event.key);
            }
        });
        
        this.textInput.addEventListener('keydown', (event) => {
            if (event.keyCode === 39 || event.keyCode === 37) {
                event.stopPropagation();
            }
        });
    }
    
    ...
}

All right, we now have almost everything set up. Here’s the last thing we need to do. We want to set focus to the input in our custom cell editor as soon as the user activates the edit mode. Conveniently, the afterGuiAttached method that can be used for that purpose:

class NumericCellEditor {
    ...
    afterGuiAttached() {
        if (this.textInput) this.textInput.focus();
    };
}

So here is the full code for you to copy/paste it:

class NumericCellEditor {
    init(params) {
        this.textInput = document.createElement('input');
        this.textInput.value = params.value;

        this.textInput.addEventListener('keypress', (event) => {
            if (!isNumeric(event)) {
                event.preventDefault();
            }

            function isNumeric(event) {
                return /\d/.test(event.key);
            }
        });
        
        this.textInput.addEventListener('keydown', (event) => {
            if (event.keyCode === 39 || event.keyCode === 37) {
                event.stopPropagation();
            }
        });
    }

    getGui() {
        return this.textInput;
    }

    afterGuiAttached() {
        if (this.textInput) this.textInput.focus();
    };

    getValue() {
        return Number(this.textInput.value);
    };
}

Register the component

Similarly to a custom cell renderer, we need to register our custom cell editor in the components and specify it as an editor for the “Price” column:

    const columnDefs = [
        {headerName: 'Make', field: 'make'},
        {headerName: 'Model', field: 'model'},
        {
            headerName: 'Price',
            field: 'price',
            editable: true,

            /* custom cell editor */
            cellEditor: 'numericCellEditor',
        }
    ];

    const gridOptions = {
        columnDefs: columnDefs,
        enableSorting: true,
        enableFilter: true,
        components: {

            /* custom cell editor component */
            numericCellEditor: NumericCellEditor,
        }
    };

And that’s it. We’re all good now to start working on our last task of implementing a custom filter.


Custom column filter

Filtering is one of the most useful features of data grids. It allows users to zoom in on a particular set of records. Our JavaScript datagrid provides a simple string filtering out of the box. But when you need you custom filtering logic types, you can easily implement it with a custom filter component. The component we we’ll be working on filters out cars based on a price range.

Enabling filtering

Before we start working on our filter component, we need to enable filtering in the grid:

    const gridOptions = {
        columnDefs: columnDefs,
        enableSorting: true,
        
        /* enable filtering */
        enableFilter: true,
    };

Define custom component

To implement a custom filter we follow the familiar customization approach and define a component. Our filter will be rendered as an input with a button to apply the filter. Let’s create this DOM structure in the init method:

class RangeFilter {
    ...
    init(params) {
        this.form = document.createElement('form');
        this.input = document.createElement('input');
        const button = document.createElement('button');

        this.form.appendChild(this.input);
        this.form.appendChild(button);

        this.input.name = 'filter';
        this.input.value = this.filter;
        button.textContent = 'Apply';
    }
}

Our filter allows users to specify a range for a car price in the form of lower boundary — upper boundary:

We’ll introduce the filter property to store the current filtering condition in the component’s state. We also want to pre-fill the input with the current filtering condition when it’s shown. To do that, we’ll assign the value to input when we create it. Here’s the code that implements this:

class RangeFilter {
    ...
    init(params) {
        this.form = document.createElement('form');
        this.input = document.createElement('input');
        const button = document.createElement('button');

        ...
        
        this.filter = '';
        this.input.value = this.filter;
    }
}

Then we need to process user input and save it to the component’s state. To do that, we’ll register an event listener on the form and process input when the form is submitted using Apply button. Also, whenever there’s a change in the filtering condition, we not only need to update the state, but also notify ag-Grid about the change. We can do that by calling the filterChangedCallback that ag-Grid provides for us through the parameters in the agInit method:

@Component({
    selector: 'app-range-filter-cell',
    template: `
    <form (submit)="onSubmit($event)">
      <input name="filter"/>
      <button>Apply</button>
    </form>
  `
})
export class RangeFilterComponent {
    filter = '';

    onSubmit(event) {
        event.preventDefault();

        const filter = event.target.elements.filter.value;

        if (this.filter !== filter) {
            this.filter = filter;

            /* notify ag-Grid about the change */
            this.params.filterChangedCallback();
        }
    }
}

We’re done now with the user interface.

Next, we need to implement the doesFilterPass function that performs filtering. It’s called by ag-Grid to determine whether a value passes the current filtering condition or not. The other function called by ag-Grid that we need to implement is isFilterActive. It determines whether the filter has any filtering condition to apply.

Let’s implement this functionality on our component:

class RangeFilter {
    ...
    isFilterActive() {
        return this.filter !== '';
    }

    doesFilterPass(params) {
        const filter = this.filter.split('-');
        const gt = Number(filter[0]);
        const lt = Number(filter[1]);
        const value = this.params.valueGetter(params.node);

        return value >= gt && value <= lt;
    }
}

Notice that we also receive the valueGetter function through parameters from ag-Grid. We then use it to retrieve the current value of a cell.

Filter presets

ag-Grid implements API that can be used to activate and deactivate filters on demand. Usually this API is triggered by some UI element, like button:

For this functionality to work, our custom component should implement two methods — setModel and getModel. ag-Grid calls them when the filter is activated and to obtain the current filtering condition. Here’s how we implement these methods in code:

class RangeFilter {
    ...
    getModel() {
        return {filter: this.filter};
    }

    setModel(model) {
        this.filter = model ? model.filter : '';
    }
}

We’re almost ready. The last thing we need to do is bring focus to input. To do that we’ll use the familiar afterGuiAttached method:

class RangeFilter {
    ...
    afterGuiAttached(params) {
        this.input.focus();
    }
}

Complete implementation

So here is the full code of our component:

class RangeFilter {
    init(params) {
        this.params = params;

        this.filter = '';

        this.form = document.createElement('form');
        this.input = document.createElement('input');
        const button = document.createElement('button');

        this.form.appendChild(this.input);
        this.form.appendChild(button);

        this.input.name = 'filter';
        this.input.value = this.filter;
        button.textContent = 'Apply';

        this.form.addEventListener('submit', (event) => {
            event.preventDefault();

            let filter = event.target.elements.filter.value;

            if (this.filter !== filter) {
                this.filter = filter;
                this.params.filterChangedCallback();
            }
        });
    }

    getGui() {
        return this.form;
    }

    isFilterActive() {
        return this.filter !== '';
    }

    doesFilterPass(params) {
        const filter = this.filter.split('-');
        const gt = Number(filter[0]);
        const lt = Number(filter[1]);
        const value = this.params.valueGetter(params.node);

        return value >= gt && value <= lt;
    }

    getModel() {
        return {filter: this.filter};
    }

    setModel(model) {
        this.filter = model ? model.filter : '';
    }

    afterGuiAttached(params) {
        this.input.focus();
    }
}

Register the component

Once we have our component ready, we need to register it in the components and specify it as a filter for the “Price” column:

    const columnDefs = [
        {headerName: 'Make', field: 'make'},
        {headerName: 'Model', field: 'model'},
        {
            headerName: 'Price',
            field: 'price',
            
            /* custom column filter */
            filter: 'rangeFilter',
        }
    ];

    const gridOptions = {
        columnDefs: columnDefs,
        enableSorting: true,
        enableFilter: true,
        components: {
            
            /* custom column filter component */
            rangeFilter: RangeFilter
        }
    };

Play With The Example Here

You can download the sample  from the github repository, in the customization-demo folder.

Build Your Own JavaScript App With ag-Grid!

I hope that the examples above clearly demonstrated how easy it is to customise and configure our JavaScript grid.

Now take the next step and start using ag-Grid in your project!

Learn more about AG Grid — high performance JavaScript Data Grid. We write the code to visualise data in interactive tables so you can concentrate on writing the application code. We support multiple frameworks: Angular, Vue, React so you can pick the best framework for your needs.

Read more posts about...