How to implement a custom column search in React material-table with Django REST Framework

Cristian Gabor
6 min readJun 22, 2020

--

I am currently developing a new React/Django application for a customer who would like to have the option to perform a search for multiple fields according to the columns listed in a table.

So…what I mean by that?

For each column in a table there should be a search input field which will pick up the word the user wants to search for, in that column and make a request to the back-end server (Django in my case) to GET and update the react state with the new matching data.

As you can see in the above image, each column (except actions) has an input field where the user will do the search.

Material-table is a great react data table component which is based on material-ui. It already has a lot of functionalities for manipulating list data entries built into it.

Their documentation is quite good and straight to the point. Although in my opinion some improvements would be great since in some of the times I caught myself searching for more information about a particular functionality in their github page.

Enough words, let’s get down to business…

When describing the steps I have followed, I already assume that you have the React and Django project set up. I do also assume that there is already an authentication token based system in place.

import axios from ‘axios’;const axiosInstance = axios.create({
baseURL: ‘http://localhost:8000/api/',
timeout: 5000,
headers: {
‘Authorization’: “JWT “ + localStorage.getItem(‘access_token’),
‘Content-Type’: ‘application/json’,
‘accept’: ‘application/json’
}
});

Step 1) Define an axios instance with predefined headers. Filename: axiosApi.js

This is intended to let axios know how to use the authentication Jason token system when making requests on the back-end. If you do not have an authentication system in place, you can skip this step and call axios.get method without having a predefined axios instance.

import React from ‘react’;
import MaterialTable from “material-table”;
import { TablePagination } from ‘@material-ui/core’;
import axiosInstance from “../axiosApi”;
import "../css/service.component.css";
export class Services extends React.Component {
constructor() {
super();
this.state = {
products: [],
count: null,
pageSize: 10,
currentPage: 0,
nextPage: 0
}
this.getData = this.getData.bind(this);
}
}

Step 2) Write the initial Service component structure. File service.component.js

render() {
return (
<MaterialTable
title= ‘Service table’
columns={[
{ title: “Service Name”, field: “nom_service” },
{ title: “Service Number”, field: “nr_service”},
{ title: “Programme LPX”, field: “no_programe”},
{ title: “Actif”, field: “actif” }
]}
data={this.state.products}
totalCount={this.state.count}
options={{
pageSize:this.state.pageSize,
search: false,
headerStyle: {
backgroundColor: ‘#545955’,
color: ‘#FFF’,
fontSize:18,
rowStyle: { “&:hover”: {backgroundColor: “#EEE”}}
}
}}
components={{
Pagination: props => {
return (
<TablePagination {…props}
rowsPerPageOptions={[10, 20, 50]}
count={this.state.count+1}
page={this.state.currentPage}
rowsPerPage={this.state.pageSize }
onChangePage={(e, page) => {
this.setState({
currentPage:page,
nextPage: page+1
}, () => { this.changetoNextPage()})
}}
onChangeRowsPerPage={(event) => {
props.onChangeRowsPerPage(event);
this.setState({pageSize: event.target.value }, () => {
this.changeRowsPerPage();})
}}/>
)}
}}

/>)
}

Step 3) Write the render() method with the return. We call <MaterialTable/> component and pass to it the initial props that will populate material-table with the initial values. this.state.products hold the actual data that will be displayed in the material-table row. To create the columns, the props accept columns with an array of objects. Each object will need to have a field property with a value matching one of the this.state.products attribute.

async getData(page_size=9, page=1,nom_service=null,
service_number=null, no_programe=null) {
let obj = { params : {
“factory”:id,
“page_size”: page_size,
“page”: page
}}

if(nom_service){
obj.params.nom_service = nom_service
} else if (nom_programme) {
obj.params.no_programe = no_programe
} else if (service_number) {
obj.params.service_number = service_number

} else {/* do nothing*/}
try {
const response = await axiosInstance.get(‘get/servicecatalog/filter/’, obj);
let data = response.data.service_catalog_list.results
let count = response.data.service_catalog_list.count

let search_function = { “nom_service”: <input type=”text” className=”empty” placeholder=”&#xF002;” onKeyDown={this.searchServiceName}/>,
“nr_service”: <input type=”text” className=”empty” placeholder=”&#xF002;” onKeyDown={this.searchServiceNumber}/>,
“nom_programe”: <input type=”text” className=”empty” placeholder=”&#xF002;” onKeyDown={this.searchProgarmme}/>}


data = [search_function, …data]
this.setState({products: data, count: count})

} catch(error){
console.log(“Error: “, JSON.stringify(error, null, 4));
throw error;
}
}

Step 4) Since axios returns a promise, we can write an async function which will update the component state when the request is successful.

To have a versatile getData() method, some default parameters will be provided. They can be overridden when the method gets called

An obj object is created with default attributes. Depending on which parameters getData() is called, the obj will have new values appended.

Axios calls the back-end api and stores the response into a response variable

search_function stores the input search field for each column in one row . Each input has onkeyDown={…} attribute which will call a different method to handle the search submit keyword.

search_function object is then added at the start of data array in order to have the search row at the start of the table.

Finally we update the state products and count with the new data .

constructor() {
super();
this.state = {
products: [],
count: null,
pageSize: 10,
currentPage: 0,
nextPage: 0
}
this.getData = this.getData.bind(this);
this.changetoNextPage = this.changetoNextPage.bind(this);
this.changeRowsPerPage = this.changeRowsPerPage.bind(this);
this.searchServiceName = this.searchServiceName.bind(this);
this.searchServiceNumber = this.searchServiceNumber.bind(this);
this.searchProgarmme = this.searchProgarmme.bind(this);
this.searchActif = this.searchActif.bind(this);
}

changeRowsPerPage(){
this.getData(this.state.pageSize-1);
}
changetoNextPage(){
this.getData(this.state.pageSize-1, this.state.nextPage);;
}
searchServiceName(event){
if (event.key === ‘Enter’) {
this.getData(undefined, undefined, event.target.value)
}
}
searchServiceNumber(event){
if (event.key === ‘Enter’) {
this.getData(undefined, undefined, undefined, event.target.value)
}
}
searchProgarmme(event){
if (event.key === ‘Enter’) {
this.getData(undefined, undefined, undefined, undefined, event.target.value)
}
}

Step 4) We will update the constructor to have the new method bindings and write the methods for each column search. searchServiceName(), searchServiceNumber(), searchProgarmme() methods take as parameter an event passed by onkeyDown and if the key which was pressed is enter, getData() will be called.

Since we will be handling a large set of data, we do not want to load all of it into React state. This will most probably make the application consume a lot of memory. The solution to this problem is to slice the data and send it to react as different pages ( Django pagination).

changeRowsPerPage(), changetoNextPage() will handle the changes in pages and changes in rows/page.

Django API

from .serializers import ServiceCatalogSerializer
from .models import ServiceCatalog
from rest_framework.pagination import PageNumberPagination
from .pagination import PaginationHandlerMixin
class BasicPagination(PageNumberPagination):
page_size = 5
page_size_query_param = ‘page_size’
class ServiceList(APIView, PaginationHandlerMixin):
pagination_class = BasicPagination
serializer_class = ServiceCatalogSerializer
def get(self,request, format=None, *args, **kwargs):
service_name = None
nom_programme = None
service_number = None

try:
service_name = request.query_params[‘nom_service’]
except:
pass
try:
nom_programme = request.query_params[‘no_programme’]

except:
pass

try:
service_number = request.query_params[‘service_number’]
except:
pass
if(service_name):
instance = ServiceCatalog.objects.all().filter(nom_service__startswith= str(service_name))

elif(service_number):
instance = ServiceCatalog.objects.all().filter(nr_service = int(service_number))

elif(nom_programme):
instance = ServiceCatalog.objects.all().filter(nom_programme__startswith=str(nom_programme))

else:
instance = ServiceCatalog.objects.all().filter(usine_id=int(usine_id))


page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_paginated_response(self.serializer_class(page,many=True).data)
else:
serializer = self.serializer_class(instance, many=True)

return Response({“service_catalog_list”: serializer.data})

views.py

Step 5) We will implement the API view which will handle the axios request. We identify which params were provided when accessing the API. Depending on the provided params, we retrieve the objects found in the model filter.

Then…we will return a paginated response from the serializer back to the client.

class PaginationHandlerMixin(object):
@property
def paginator(self):
if not hasattr(self, ‘_paginator’):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
else:
pass
return self._paginator

def paginate_queryset(self, queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset,
self.request, view=self)
def get_paginated_response(self, data):
assert self.paginator is not None
return self.paginator.get_paginated_response(data)

pagination.py

Step 6) Implement the pagination functionality

from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ServiceList
urlpatterns = [
path(‘get/servicecatalog/’, ServiceList.as_view(), name=’service_list’),

]

urls.py

Step 7) Do not forget about the urls….

input.empty {
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
text-decoration: inherit;
}

service.component.css

Step 8) We also want to have the search icon in the input fields.

We should now have a working material-table column search function.

Conclusion

Although this search solution is a sort of workaround rather than a fully supported feature from material-table it does perform quite well at searching in the back-end the data models and updating the component state.

--

--