Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save cristian-spiescu/30b0781745066db3e27a10198f788228 to your computer and use it in GitHub Desktop.
Save cristian-spiescu/30b0781745066db3e27a10198f788228 to your computer and use it in GitHub Desktop.
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { NavLink } from 'react-router-dom';
import { reactFormatter, ReactTabulator } from 'react-tabulator';
import { IProps } from "react-tabulator/lib/ConfigUtils";
import 'react-tabulator/lib/css/semantic-ui/tabulator_semantic-ui.min.css'; // theme
import { Button, Segment } from 'semantic-ui-react';
//@ts-ignore
import StoryRouter from 'storybook-react-router';
import "tabulator-tables"; // this actually imports the types from @types/tabulator-tables
// cannot use import; I think that @types/tabulator-tables are not correctly implemented
const Tabulator = require("tabulator-tables");
/**
* Initially we wanted to enrich more functions. Hence this function meant to be reused several times.
*/
function extendFunction(object: any, func: string, beforeLogic: Function, afterLogic?: Function) {
const originalFunc = func + "Original";
if (!object[originalFunc]) {
// we store the original function in the object and not as a local var to support the hotswap
// flow; w/o this, at each hotswap, we'd enrich the already enriched function
object[originalFunc] = object[func];
} // else probably hot code swap / Storybook
object[func] = function () {
const result = beforeLogic && beforeLogic.apply(this, arguments);
object[originalFunc].apply(this, arguments);
afterLogic && afterLogic.apply(this, [result]);
console.log(func)
}
}
// monkeypatching tabulator to add a handler onWipeElements; we need an instance to grab the prototype of RowManager from an actual instance;
// the corresponding class is not exported
const tabulator: Tabulator = new Tabulator(globalThis.document.createElement("div"));
// extendFunction(Object.getPrototypeOf(tabulator.rowManager), "_clearVirtualDom", function (this: any) { this.table.onWipeElements?.(); });
extendFunction(Object.getPrototypeOf(tabulator.rowManager), "_wipeElements", function (this: any) { this.table.onWipeElements?.(); });
tabulator.destroy();
type ReactTabulatorExtProps = IProps & { propsForFormattersReact?: any };
class ReactTabulatorExt extends React.Component<ReactTabulatorExtProps> {
protected enrichedColumns: any[];
protected cellsWithFormattersReact: [Tabulator.CellComponent, any][] = [];
constructor(props: ReactTabulatorExtProps) {
super(props);
this.onSetRef = this.onSetRef.bind(this);
this.enrichedColumns = props.columns.map(column => {
const result = { ...column };
if (column.formatterReact) {
result.formatter = (cell: Tabulator.CellComponent, formatterParams: any, onRendered: any) => {
// saving the cell; we are interested mostly in it's domElement
this.cellsWithFormattersReact.push([cell, column.formatterReact]);
// deffer a bit the update; because the domElement is not yet on screen; we use forceUpdate()
// because we don't keep the cells in the state; and we don't keep in the state to make a
// slight amount of economy: i.e. mutate the field above, instead of creating a new array
// at each step
onRendered(() => this.forceUpdate());
}
}
return result;
});
}
/**
* A separate function because inline functions cause a lot of calls w/ not-null, null, etc.
*/
protected onSetRef(ref: ReactTabulator) {
if (!ref) {
// for Storybook (I think?); sometimes we are called w/ null
return;
}
// using timeout because ReactTabulator sets this field a bit later, apparently async
setTimeout(() => ref.table.onWipeElements = () => this.cellsWithFormattersReact = []);
}
render() {
return <>
{/* we render here all the cells; but using portals, the actual DOM goes in the DOM elements saved above */}
{this.cellsWithFormattersReact.map(cell => ReactDOM.createPortal(React.createElement(cell[1], { ...this.props.propsForFormattersReact, data: cell[0].getData() }), cell[0].getElement()))}
<ReactTabulator ref={this.onSetRef} {...this.props} columns={this.enrichedColumns} />
</>;
}
}
export default {
title: 'Tabulator'
};
function SimpleButton(props: any) {
const rowData = props.cell._cell.row.data;
const cellValue = props.cell._cell.value || 'Edit | Show';
return <button onClick={() => alert(rowData.name)}>{cellValue}</button>
}
/**
* This component has access to props passed by the user + things coming from contexts, such
* as router related info, necessary e.g. for NavLink.
*/
function Renderer(props: any) {
return <NavLink to="/myPage">Hello {props.data.name} {props.counter}</NavLink>;
}
const columns = [
{ title: 'Name', field: 'name', width: 150 },
{ title: 'Age', field: 'age', hozAlign: 'left', formatter: 'progress' },
{ title: 'Favourite Color', field: 'color' },
{ title: 'Date Of Birth', field: 'dob', sorter: 'date' },
{ title: 'Rating', field: 'rating', hozAlign: 'center', formatter: 'star' },
{ title: 'Passed?', field: 'passed', hozAlign: 'center', formatter: 'tickCross' },
// The react-tabulator way
{ title: 'Custom1', field: 'custom', hozAlign: 'center', editor: 'input', formatter: reactFormatter(<SimpleButton />) },
// Our way
{ title: 'Custom2', field: 'custom', hozAlign: 'center', formatterReact: Renderer },
];
const initialData = [
{ id: 1, name: 'Oli Bob', age: '12', color: 'red', dob: '01/01/1980', rating: 5, passed: true, pets: ['cat', 'dog'] },
{ id: 2, name: 'Mary May', age: '1', color: 'green', dob: '12/05/1989', rating: 4, passed: true, pets: ['cat'] },
{ id: 3, name: 'Christine Lobowski', age: '42', color: 'green', dob: '10/05/1985', rating: 4, passed: false },
{ id: 4, name: 'Brendon Philips', age: '125', color: 'red', dob: '01/08/1980', rating: 4.5, passed: true },
{ id: 5, name: 'Margret Marmajuke', age: '16', color: 'yellow', dob: '07/01/1999', rating: 4, passed: false },
{ id: 6, name: 'Van Ng', age: '37', color: 'green', dob: '06/10/1982', rating: 4, passed: true, pets: ['dog', 'fish'] },
{ id: 7, name: 'Duc Ng', age: '37', color: 'yellow', dob: '10/10/1982', rating: 4, passed: true, pets: ['dog'] }
];
for (let i = 8; i < 100; i++) {
initialData.push({ id: i, name: 'Oli Bob' + i, age: '12', color: 'red', dob: '01/01/1980', rating: 5, passed: true, pets: ['cat', 'dog'] });
}
export const Tabulator1 = () => {
const [counter, setCounter] = useState(0);
let [data, setData] = useState(initialData);
return <>
<Segment>
<Button onClick={() => setData(data.length ? [] : initialData)}>Toggle data</Button>
<Button onClick={() => setCounter(counter + 1)}>Modify something in the state, used by the renderers: {counter}</Button>
<Button onClick={() => setData([...data, ...data])}>More data</Button>
<Button onClick={() => { data = [...data]; data[0] = { ...data[0] }; data[0].name += " a"; setData(data) }}>Modify first</Button>
</Segment>
<ReactTabulatorExt className="celled" columns={columns} data={data} options={{
movableColumns: true, responsiveLayout: 'collapse',
}} propsForFormattersReact={{ counter: counter }} />
</>
}
Tabulator1.story = { decorators: [StoryRouter({}, { initialEntries: ['/myPage'] })] }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment