@ashvin27/react-datatable
Version:
ReactDatatable is a component which provide ability to create multifunctional table using single component like jQuery Datatable
611 lines (564 loc) • 21.7 kB
JavaScript
/**
* This is the React Component for ReactDatatable
*
* @package ReactDatatable
* @author Ashvin Patel(patelash212@gmail.com)
* @date 14 Dec, 2018
*/
import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import _ from 'lodash';
import './assets/css/style.css';
import TableHeader from './components/TableHeader';
import TableFooter from './components/TableFooter';
import style from './style';
class ReactDatatable extends Component {
constructor(props) {
super(props);
this.exportExcelRef = React.createRef();
this.sortColumn = this.sortColumn.bind(this);
this.numPages = this.numPages.bind(this);
this.exportToExcel = this.exportToExcel.bind(this);
this.exportToPDF = this.exportToPDF.bind(this);
this.exportToCSV = this.exportToCSV.bind(this);
this.onChange = this.onChange.bind(this);
this.filterRecords = this.filterRecords.bind(this);
this.filterData = this.filterData.bind(this);
this.sortRecords = this.sortRecords.bind(this);
this.config = {
button: {
excel: (props.config && props.config.button && props.config.button.excel) ? props.config.button.excel : false,
print: (props.config && props.config.button && props.config.button.print) ? props.config.button.print : false,
csv: (props.config && props.config.button && props.config.button.csv) ? props.config.button.csv : false,
extra : (props.config && props.config.button && props.config.button.extra) ? props.config.button.extra : false,
},
filename: (props.config && props.config.filename) ? props.config.filename : "table",
key_column: props.config && props.config.key_column ? props.config.key_column : "id",
language: {
length_menu: (props.config && props.config.language && props.config.language.length_menu) ? props.config.language.length_menu : "Show _MENU_ records per page",
filter: (props.config && props.config.language && props.config.language.filter) ? props.config.language.filter : "Search in records...",
info: (props.config && props.config.language && props.config.language.info) ? props.config.language.info : "Showing _START_ to _END_ of _TOTAL_ entries",
pagination: {
first: (props.config && props.config.language && props.config.language.pagination && props.config.language.pagination.first) ? props.config.language.pagination.first : "First",
previous: (props.config && props.config.language && props.config.language.pagination && props.config.language.pagination.previous) ? props.config.language.pagination.previous : "Previous",
next: (props.config && props.config.language && props.config.language.pagination && props.config.language.pagination.next) ? props.config.language.pagination.next : "Next",
last: (props.config && props.config.language && props.config.language.pagination && props.config.language.pagination.last) ? props.config.language.pagination.last : "Last"
},
no_data_text: (props.config && props.config.language && props.config.language.no_data_text) ? props.config.language.no_data_text : 'No rows found',
loading_text: (props.config && props.config.language && props.config.language.loading_text) ? props.config.language.loading_text : "Loading..."
},
length_menu: (props.config && props.config.length_menu) ? props.config.length_menu : [10, 25, 50, 75, 100],
show_length_menu: (props.config.show_length_menu != undefined) ? props.config.show_length_menu : true,
show_filter: (props.config.show_filter != undefined) ? props.config.show_filter : true,
show_pagination: (props.config.show_pagination != undefined) ? props.config.show_pagination : true,
show_info: (props.config.show_info != undefined) ? props.config.show_info : true,
show_first: (props.config.show_first != undefined) ? props.config.show_first : true,
show_last: (props.config.show_last != undefined) ? props.config.show_last : true,
pagination: (props.config.pagination) ? props.config.pagination : 'basic'
};
this.state = {
is_temp_page: false,
filter_value: "",
page_size: (props.config.page_size) ? props.config.page_size : 10,
page_number: 1,
sort: (props.config && props.config.sort) ? props.config.sort : false
};
}
filterRecords(e) {
let value = e.target.value;
this.setState({
page_number: 1,
filter_value: value
}, () => {
this.onChange();
});
}
changePageSize(e) {
let value = e.target.value;
this.setState({
page_size: value
}, () => {
this.onChange();
});
}
sortColumn(event, column, sortOrder) {
if (!column.sortable) return false;
let newSortOrder = (sortOrder == "asc") ? "desc" : "asc";
this.setState({
'sort': { column: column.key, order: newSortOrder }
}, () => {
this.onChange();
});
}
paginate(records) {
let page_size = this.state.page_size;
let page_number = this.state.page_number;
--page_number; // because pages logically start with 1, but technically with 0
return records.slice(page_number * page_size, (page_number + 1) * page_size);
}
numPages(totalRecord){
return Math.ceil(totalRecord / this.state.page_size);
}
isLast() {
// because for empty records page_number will still be 1
if(this.pages == 0){
return true;
}
if (this.state.page_number == this.pages) {
return true
} else {
return false;
}
}
isFirst() {
if (this.state.page_number == 1) {
return true;
} else {
return false;
}
}
goToPage(e, pageNumber){
e.preventDefault();
if(this.state.page_number == pageNumber){
return;
}
let pageState = {
previous_page: this.state.page_number,
current_page: pageNumber
};
this.setState({
is_temp_page: false,
page_number: pageNumber
}, () => {
this.props.onPageChange(pageState);
this.onChange();
});
}
firstPage(e) {
e.preventDefault();
if(this.isFirst()) return;
this.goToPage(e, 1);
}
lastPage(e) {
e.preventDefault();
if(this.isLast()) return;
this.goToPage(e, this.pages);
}
previousPage(e) {
e.preventDefault();
if(this.isFirst()) return false;
this.goToPage(e, this.state.page_number - 1);
}
nextPage(e) {
e.preventDefault();
if(this.isLast()) return;
this.goToPage(e, parseInt(this.state.page_number) + 1);
}
onPageChange(e, isInputChange = false) {
if(isInputChange){
this.setState({
is_temp_page : true,
temp_page_number: e.target.value
});
} else {
if (e.key === 'Enter') {
let pageNumber = e.target.value;
this.goToPage(e, pageNumber);
}
}
}
onPageBlur(e){
let pageNumber = e.target.value;
this.goToPage(event, pageNumber);
}
strip(html){
let doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || "";
}
getExportHtml(){
let tableHtml = "<table>";
tableHtml += "<thead>";
tableHtml += "<tr>";
for (let column of this.props.columns) {
tableHtml += "<th>" + column.text + "</th>";
}
tableHtml += "</tr>";
tableHtml += "</thead>";
tableHtml += "<tbody>";
// Filter records before export
let filterRecords = this.props.records;
if(this.props.dynamic === false){
let records = this.sortRecords(),
filterValue = this.state.filter_value;
filterRecords = records;
if (filterValue) {
filterRecords = this.filterData(records);
}
}
for (let i in filterRecords) {
let record = filterRecords[i];
tableHtml += "<tr>";
for (let column of this.props.columns) {
if (column.cell && typeof column.cell === "function") {
let cellData = ReactDOMServer.renderToStaticMarkup(column.cell(record, i));
cellData = this.strip(cellData);
tableHtml += "<td>" + cellData + "</td>";
}else if (record[column.key]) {
tableHtml += "<td>" + record[column.key] + "</td>";
} else {
tableHtml += "<td></td>";
}
}
tableHtml += "</tr>";
}
tableHtml += "</tbody>";
tableHtml += "</table>";
return tableHtml;
}
exportToExcel(){
let downloadLink, dataType = 'application/vnd.ms-excel';
let tableHtml = this.getExportHtml();
// Specify file name
let filename = this.config.filename ? this.config.filename + '.xls':'table.xls';
// Create download link element
downloadLink = document.createElement("a");
if(navigator.msSaveOrOpenBlob){
let blob = new Blob(['\ufeff', tableHtml], {
type: dataType
});
navigator.msSaveOrOpenBlob(blob, filename);
}else{
// Create a link to the file
downloadLink.href = 'data:' + dataType + ', ' + tableHtml;
// Setting the file name
downloadLink.download = filename;
//triggering the function
downloadLink.click();
}
}
exportToPDF() {
let tableHtml = this.getExportHtml();
let style = "<style>";
style = style + "table {width: 100%;font: 17px Calibri;}";
style = style + "table, th, td {border: solid 1px #DDD; border-collapse: collapse;";
style = style + "padding: 2px 3px;text-align:left;}";
style = style + "</style>";
let win = window.open('', '_blank');
win.document.write('<html><head>');
win.document.write('<title>' + this.config.filename + '</title>');
win.document.write(style);
win.document.write('</head>');
win.document.write('<body>');
win.document.write('<h1>' + this.config.filename + '</h1>');
win.document.write(tableHtml);
win.document.write('</body></html>');
win.print();
win.close();
}
convertToCSV(objArray){
let array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
let str = '';
for (let i = 0; i < array.length; i++) {
let line = '';
for (let index in array[i]) {
if (line != '') line += ','
line += array[i][index];
}
str += line + '\r\n';
}
return str;
}
exportToCSV(){
let headers = {};
// add columns in sheet array
for (let column of this.props.columns) {
headers[column.key] = '"' + column.text + '"';
}
// Filter records before export
let filterRecords = this.props.records;
if(this.props.dynamic === false){
let records = this.sortRecords(),
filterValue = this.state.filter_value;
filterRecords = records;
if (filterValue) {
filterRecords = this.filterData(records);
}
}
let records = [];
// add data rows in sheet array
for (let i in filterRecords) {
let record = filterRecords[i],
newRecord = {};
for (let column of this.props.columns) {
if (column.cell && typeof column.cell === "function") {
let cellData = ReactDOMServer.renderToStaticMarkup(column.cell(record, i));
cellData = this.strip(cellData);
newRecord[column.key] = cellData;
} else if (record[column.key]) {
let colValue = record[column.key];
colValue = (typeof colValue === "string") ? colValue.replace(/"/g, '""') : colValue;
newRecord[column.key] = '"' + colValue + '"';
} else {
newRecord[column.key] = "";
}
}
records.push(newRecord);
}
if (headers) {
records.unshift(headers);
}
// Convert Object to JSON
let jsonObject = JSON.stringify(records);
let csv = this.convertToCSV(jsonObject);
let exportedFilename = this.config.filename + '.csv' || 'export.csv';
let blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
if (navigator.msSaveBlob) { // IE 10+
navigator.msSaveBlob(blob, exportedFilename);
} else {
let link = document.createElement("a");
if (link.download !== undefined) { // feature detection
// Browsers that support HTML5 download attribute
let url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", exportedFilename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
link.remove();
}
}
}
onChange(){
let tableData = {
filter_value: this.state.filter_value,
page_number: this.state.page_number,
page_size: this.state.page_size,
sort_order: this.state.sort
};
this.props.onChange(tableData);
}
filterData(records) {
let filterValue = this.state.filter_value;
return records.filter((record) => {
let allow = false;
_.each(this.props.columns, (column, key) => {
if (record[column.key]) {
allow = _.includes(record[column.key].toString().toLowerCase(), filterValue.toString().toLowerCase()) ? true : allow;
}
});
return allow;
});
}
sortRecords(){
if(this.state.sort){
return _.orderBy(this.props.records, o => {
let colVal = o[this.state.sort.column];
let typeofColVal = typeof colVal;
if (typeofColVal == "string") {
if (isNaN(colVal)) {
return new String(colVal.toLowerCase());
} else {
return new Number(colVal);
}
} else if (typeofColVal == "number") {
return new Number(colVal);
}
}, [this.state.sort.order]);
} else {
return this.props.records;
}
}
render() {
let filterRecords, totalRecords, pages, isFirst, isLast;
if(this.props.dynamic === false){
let records = (this.props.onSort) ? this.props.onSort(this.state.sort.column, this.props.records, this.state.sort.order) : this.sortRecords(),
filterValue = this.state.filter_value;
filterRecords = records;
if (filterValue) {
filterRecords = this.filterData(records);
}
totalRecords = Array.isArray(filterRecords) ? filterRecords.length : 0;
pages = this.pages = this.numPages(totalRecords);
isFirst = this.isFirst();
isLast = this.isLast();
filterRecords = Array.isArray(filterRecords) ? this.paginate(filterRecords) : [];
}else{
filterRecords = this.props.records;
totalRecords = this.props.total_record;
pages = this.pages = this.numPages(totalRecords);
isFirst = this.isFirst();
isLast = this.isLast();
}
let startRecords = (this.state.page_number * this.state.page_size) - (this.state.page_size - 1);
let endRecords = this.state.page_size * this.state.page_number;
endRecords = (endRecords > totalRecords) ? totalRecords : endRecords;
let lengthMenuText = this.config.language.length_menu;
lengthMenuText = lengthMenuText.split('_MENU_');
let paginationInfo = this.config.language.info;
paginationInfo = paginationInfo.replace('_START_', (this.state.page_number == 1) ? 1 : startRecords);
paginationInfo = paginationInfo.replace('_END_', endRecords);
paginationInfo = paginationInfo.replace('_TOTAL_', totalRecords);
return (
<div className="as-react-table" id={(this.props.id) ? this.props.id + "-container" : ""}>
<TableHeader
config={this.config}
id={this.props.id}
lengthMenuText={lengthMenuText}
recordLength={(this.props.dynamic) ? this.props.total_record : this.props.records.length}
filterRecords={this.filterRecords.bind(this)}
changePageSize={this.changePageSize.bind(this)}
exportToExcel={this.exportToExcel.bind(this)}
exportToCSV={this.exportToCSV.bind(this)}
exportToPDF={this.exportToPDF.bind(this)}
extraButtons={this.props.extraButtons}/>
<div className="row table-body asrt-table-body" style={style.table_body} id={(this.props.id) ? this.props.id + "-table-body" : ""}>
<div className="col-md-12">
<table className={this.props.className} id={this.props.id}>
<thead className={this.props.tHeadClassName ? this.props.tHeadClassName : ''}>
<tr>
{
this.props.columns.map((column, index) => {
let classText = (column.sortable) ? "sortable " : "",
width = (column.width) ? column.width : "",
align = (column.align) ? column.align : "",
sortOrder = "",
columnStyle = {};
if (column.sortable && this.state.sort.column == column.key) {
sortOrder = this.state.sort.order;
classText += (sortOrder) ? " " + sortOrder : "";
columnStyle = (sortOrder == "asc") ? style.sort_asc : style.sort_desc;
}
classText += " text-" + align;
if(column.TrOnlyClassName)
classText += " " + column.TrOnlyClassName;
return (<th
key={(column.key) ? column.key : column.text}
className={classText}
width={width}
style={columnStyle}
onClick={event => this.sortColumn(event, column, sortOrder)}>
{column.text}
</th>);
})
}
</tr>
</thead>
<tbody>
{this.props.loading === true ? (
<tr>
<td colSpan={this.props.columns.length} className="asrt-td-loading" align="center">
<div className="asrt-loading-textwrap">
<span className="asrt-loading-text">
{this.config.language.loading_text}
</span>
</div>
</td>
</tr>
) : (
(filterRecords.length) ?
filterRecords.map((record, rowIndex) => {
rowIndex = _.indexOf(this.props.records, record);
return (
<tr key={record[this.config.key_column]} onClick={(e) => this.props.onRowClicked(e, record, rowIndex)}>
{
this.props.columns.map((column, colIndex) => {
if (column.cell && typeof column.cell === "function") {
return (<td className={column.className} key={(column.key) ? column.key : column.text}>{column.cell(record,rowIndex)}</td>);
}else if (record[column.key]) {
return (<td className={column.className} key={(column.key) ? column.key : column.text}>
{record[column.key]}
</td>);
}else {
return <td className={column.className} key={(column.key) ? column.key : column.text}></td>
}
})
}
</tr>
)
}) :
(
<tr>
<td colSpan={this.props.columns.length} align="center">
{this.config.language.no_data_text}
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
<TableFooter
config={this.config}
id={this.props.id}
isFirst={isFirst}
isLast={isLast}
paginationInfo={paginationInfo}
pages={pages}
page_number={this.state.page_number}
is_temp_page={this.state.is_temp_page}
temp_page_number={this.state.temp_page_number}
firstPage={this.firstPage.bind(this)}
lastPage={this.lastPage.bind(this)}
previousPage={this.previousPage.bind(this)}
nextPage={this.nextPage.bind(this)}
goToPage={this.goToPage.bind(this)}
changePageSize={this.changePageSize.bind(this)}
onPageChange={this.onPageChange.bind(this)}
onPageBlur={this.onPageBlur.bind(this)}/>
</div>
)
}
}
/**
* Define component display name
*/
ReactDatatable.displayName = 'ReactDatatable';
/**
* Define defaultProps for this component
*/
ReactDatatable.defaultProps = {
id: "as-react-datatable",
className: "table table-bordered table-striped",
columns: [],
config: {
button: {
excel: false,
print: false,
csv: false
},
filename: "table",
key_column:"id",
language: {
length_menu: "Show _MENU_ records per page",
filter: "Search in records...",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
pagination: {
first: "First",
previous: "Previous",
next: "Next",
last: "Last"
}
},
length_menu: [10, 25, 50, 75, 100],
no_data_text: "No rows found",
page_size: 10,
sort: {
column: "test",
order: "asc"
},
show_length_menu: true,
show_filter: true,
show_pagination: true,
show_info: true,
show_first: true,
show_last: true
},
dynamic: false,
records: [],
total_record: 0,
onChange: () => { },
onPageChange: () => { },
onRowClicked: () => { }
}
export default ReactDatatable;