Ag-grid plain JS cell renderer in React - javascript

I am using ag-grid in a React application. We have pretty heavy duty tables with custom cells. There are performance issues when using custom cell renderers built as React components, which is the most intuitive discourse.
The ag-grid docs state that this is probably not a good idea:
Do NOT use a framework (eg Angular or React) for the cell renderers. The grid rendering is highly customised and plain JavaScript cell renderers will work faster than framework equivalents.
But it also indicates that plain JS can be used in conjunction with frameworks like React:
It is still fine to use the framework version of ag-Grid (eg for setting ag-Grid properties etc) however because there are so many cells getting created and destroyed, the additional layer the frameworks add do not help performance and should be provided if you are having performance concerns.
Am I misinterpreting this? This seems to me that I can use just a plain JS class as a cell renderer (somehow, maybe they'll handle the integration with React?)
So I took their example code and converted it to a class instead of a function to conform to their typescript definitions:
// function to act as a class
class MyCellRenderer {
eGui: any;
eButton: any;
eValue: any;
eventListener: any;
init(params: any) {
// create the cell
this.eGui = document.createElement('div');
this.eGui.innerHTML =
'<span class="my-css-class"><button class="btn-simple">Push Me</button><span class="my-value"></span></span>';
// get references to the elements we want
this.eButton = this.eGui.querySelector('.btn-simple');
this.eValue = this.eGui.querySelector('.my-value');
// set value into cell
this.eValue.innerHTML = params.valueFormatted ? params.valueFormatted : params.value;
// add event listener to button
this.eventListener = function() {
// tslint:disable-next-line
console.log('button was clicked!!');
};
this.eButton.addEventListener('click', this.eventListener);
}
// gets called once when grid ready to insert the element
getGui() {
return this.eGui;
}
// gets called whenever the user gets the cell to refresh
refresh(params: any) {
// set value into cell again
this.eValue.innerHTML = params.valueFormatted ? params.valueFormatted : params.value;
// return true to tell the grid we refreshed successfully
return true;
}
// gets called when the cell is removed from the grid
destroy() {
// do cleanup, remove event listener from button
this.eButton.removeEventListener('click', this.eventListener);
}
}
// gets called once before the renderer is used
export default MyCellRenderer;
This builds just fine. Now when I pull up my table in the app, I get the somewhat predictable error:
MyCellRenderer(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
So it was expecting a React component exclusively? It appears that I need to provide the rendering operation anyway.
Does anyone know what's going on here/how to resolve this issue? Am I misinterpreting the documentation?
Thanks!
p.s. ag-grid is awesome!

Have just worked out how to do it. There's two sets of 'grid components' properties that you can make available for an instance of a grid for renderers and editors.
The frameworkComponents property contains ones that will be rendered using the framework you're using, such as React or Angular.
The components property contains ones that will be rendered using straight JS.
You can mix these however you wish, for example, assuming you have some renderers that have been exported using this pattern:
export const XRenderer = {
id: 'someId',
renderer: function() ... // or class, or whatever
}
// React components
const frameworkComponents = {
[CheckboxRenderer.id]: CheckboxRenderer.renderer,
[SelectRenderer.id]: SelectRenderer.renderer
};
// JavaScript components
const components = {
[RateRenderer.id]: RateRenderer.renderer
};
<Grid
columnDefs={columnDefinitions}
theme={theme}
rowData={rows}
frameworkComponents={gridComponents} // React components
components={components} // JavaScript components
onGridReady={this.onGridReady}
context={this.gridContext}
gridOptions={this.mainGridOptions}
...
/>
The information on how to do this is actually in the docs, but not in a particularly useful place: https://www.ag-grid.com/javascript-grid-components/#mixing-javascript-and-framework

Related

How to use a dojo widget inside a react component?

Is there any way to re-use a component/widget from other library inside a react component?
E.g.
export default function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>{count}</button>
<button id="btn" onClick={() => function(){
require(["dijit/form/Button", "dojo/domReady!"], function(Button) {
var button = new Button({
label: "Click Me!",
onClick: function(){ console.log("Opening a dojo dialog"); }
}, "btn");
button.startup();
});
}}>Open Dojo component</button>
<br />
</div>
);
Here, this is a basic react component. I am trying to use a dojo button inside it but apparently does not work. I am actually thinking in the lines of having a button event in the react and call some javascript function which parses the dojo widget. Not sure if that will work.
Is there a better approach?
The reason your attempt didn't work is because you're still rendering plain buttons in the React JSX, and when you click that button you're creating a new Dojo button but you're not adding it to the DOM. Even if you did add it to the DOM, React might still remove it on a later re-render because it's not part of the JSX.
We've recently undergone an integration of React into an existing Dojo application, and the method we use to put Dojo widgets into React components is as follows.
The high level overview is that we create special React components that "host" Dojo widgets inside them. I'll start from the top and work my way down.
The usage of these hosted Dojo widgets is mostly straight forward, and looks something like the following JSX
<div>
<Button
dojoProps={{ label: "Add" }}
dojoEvents={{ click: onAdd }}
/>
<Button
dojoProps={{ label: "Remove" }}
dojoEvents={{ click: onRemove }}
/>
</div>
where Button is our Dojo hosting component. The entries in the dojoProps property are passed into the constructor of the widget and into .set(...) when the properties change. Then entries in dojoEvents are passed into .on(...) when being created and when they change as well.
The Button class looks like this (it's in TS, but should be easy to translate into JS)
import * as DojoButton from "dijit/form/Button";
import { DojoInReactComponent } from "../DojoInReactComponent";
export class Button extends DojoInReactComponent<DojoButton> {
constructor(props: Button["props"]) {
super(new DojoButton(props.dojoProps), props);
}
}
We make one of these classes for each Dojo widget we wish to wrap and show in React and is re-usable throughout the project. Note that this is where the widget is created and the props are passed into the widget's constructor.
The important part of the implementation is in the DojoInReactComponent class:
import * as React from "react";
import * as _WidgetBase from "dijit/_WidgetBase";
/**
* A React component that hosts a Dojo widget
*/
export abstract class DojoInReactComponent
<W extends _WidgetBase, P extends DojoInReactComponentProps<W> = DojoInReactComponentProps<W>>
extends React.Component<P> {
/** Stores a React Ref to the actual DOMNode that we place the widget at */
private readonly widgetRef: React.RefObject<HTMLSpanElement>;
/** Cache of the registered event handles for this widget (used to cleanup handles on unmount) */
private readonly eventsRegistered: EventRegistrationCache<W> = {};
/** The actual widget that will be stored in this component */
readonly widget: W;
constructor(widget: W, props: P) {
super(props);
this.widgetRef = React.createRef();
this.widget = widget;
}
componentDidMount() {
if (!this.widgetRef.current) {
throw new Error("this.widgetRef was not set");
}
// First, set properties
this.widget.set(this.props.dojoProps ?? {});
// Then set event handlers. This is the first time it happens so
// the eventsRegistered cache is empty (ie: nothing to remove).
this.addEventHandlers(this.props.dojoEvents);
// Finally, place it at the domNode that this component created when it rendered.
this.widget.placeAt(this.widgetRef.current);
}
componentDidUpdate(prevProps: P) {
// First, update props. Note that we merge the old and new properties together here before setting to
// ensure nothing drastically changes if we don't pass in a property. If we want it to change, we need to
// explicitly pass it in to the dojoProps property in the TSX.
// This also attempts to make it obvious that not setting a property in the TSX will leave it unchanged,
// compared to it's existing value, as that's the default behaviour of Dojo widgets and .set().
const props = { ...prevProps.dojoProps ?? {}, ...this.props.dojoProps ?? {} };
this.widget.set(props);
// Then update event handlers. Note that we don't do this in a "smart" way, but instead we just remove all
// existing handlers, and then re-add the supplied ones. Generally it will mean removing and re-adding the same
// handlers, but it's much easier than trying to diff them.
this.removeAllEventHandlers();
this.addEventHandlers(this.props.dojoEvents);
}
componentWillUnmount() {
// On cleanup we need to remove our handlers that we set up to ensure no memory leaks.
this.removeAllEventHandlers();
// Finally we destroy the widget
this.widget.destroyRecursive();
}
private addEventHandlers(events: EventsType<W> | undefined) {
if (!events) {
return;
}
for (const key of keysOf(events)) {
const newHandle = this.widget.on(key as any, events[key] as any);
this.eventsRegistered[key] = newHandle;
}
}
private removeAllEventHandlers() {
for (const key of keysOf(this.eventsRegistered)) {
const handle = this.eventsRegistered[key];
if (handle) {
handle.remove();
}
delete this.eventsRegistered[key];
}
}
render() {
return <span ref={this.widgetRef}></span>;
}
}
function keysOf<T extends {}>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}
type EventsType<T extends _WidgetBase> = Record<string, () => void>;
type PropsType<T extends _WidgetBase> = Partial<T>;
export interface DojoInReactComponentProps<W extends _WidgetBase> {
dojoProps?: PropsType<W>;
dojoEvents?: EventsType<W>;
}
type EventRegistrationCache<W extends _WidgetBase> = Record<string, dojo.Handle>;
There's a bit to unpack here, but the general gist of it is:
render() produces a simple <span> element that we keep a reference to to mount our Dojo widget to shortly
componentDidMount() updates the properties and event handlers of the Dojo widget to our supplied properties. It then places the actual Dojo widget into the DOM produced by the React component
componentDidUpdate() again updates the properties and event handlers if they changes due to a re-render of the React component
componentWillUnmount() is called when the React component is being removed and does a teardown of the hosted Dojo widget by calling .destroyRecursive() to remove it from the DOM
The types down the bottom are just utility types to help TS infer everything for us, however EventsType and EventsRegistrationCache uses additional changes that are far too long to write out here, so I've replaced them with a stub (but if you're just using JS then it won't matter).
Let me know if you have any questions about it.

Alter react component state properly

I'm working at a project in which I have to display graphs.
For displaying graphs I'm using vis.js in particular react-vis-network a implementation for using parts of vis.js in React with its stateful approaches.
Initial nodes and edges are loaded before my component is mounted and are passed as props for an initial state.
I attached two eventHandler one direct to a vis.js (the underlying DOM library) and the other at a decorator (button).
The desired/expected behaviour:
A node is removed by clicking either the node or the corresponding button.
Observed behavior:
Sometimes a node is removed and sometimes a node just disappears for a few ms and is reattached but without a decorator/button.
I already tried to start with an empty state and attaching the nodes,edges in componentDidMount() but I got the same result. I hope you can give me a hint.
BTW: Is the way I use to attach components a/the right way?
Every other help to improve my class is appreciated also
class MyNetwork extends Component {
constructor(props){
super(props);
let componentNodes = [];
for (let node of props.nodes){
componentNodes.push(this.createNode(node));
}
let componentEdges = [];
for (let edge of props.edges){
componentEdges.push(this.createEdge(edge));
}
this.state = {nodes:componentNodes,edges:componentEdges};
["_handleButtonClick"].forEach(name => {
this[name] = this[name].bind(this);
});
}
createNode(node){
const Decorator = props => {
return (
<button
onClick={() =>{this._handleButtonClick(props);}}
>
Click Me
</button>
);
};
node.decorator = Decorator;
return React.createElement(Node,{...node})
}
createEdge(edge){
return React.createElement(Edge,{...edge})
}
addNode(node){
this.setState({
nodes: [...this.state.nodes, this.createNode(node)]
})
}
_handleButtonClick(e) {
if(e){
console.log("clicked node has id:" +e.id);
this.removeNode(e.id);
}
}
onSelectNode(params){
console.log(params);
window.myApp.removeNode(params[0]);
}
removeNode(id) {
let array = [...this.state.nodes]; // make a separate copy of the array
let index = array.findIndex(i => i.props.id === id );
array.splice(index, 1);
this.setState({nodes: array});
}
render() {
return (
<div id='network'>
<Network options={this.props.options} onSelectNode={this.onSelectNode}>
{[this.state.nodes]}
{[this.state.edges]}
</Network>
</div>
);
}
}
export default MyNetwork
Before clicking node 2
After clicking node 2
Update 1
I created a live example at stackblitz which isn't working yet caused by other failures I make and can't find.
The components I use are:
Network
Node
Edge
Edge and Node are extending Module
I reworked my MyNetwork component according to some mistakes xadm mentioned.
Components (espacially dynamic) shouldn't be stored in state.
I implemented two new functions nodes() and edges() // line 15-41*
key prop should be used, too.
key is used now // line 18 + 32*
Passed props cannot be modified, you still have to copy initial data
into state. State is required for updates/rerendering.
line 9*
*line numbers in live example I mentioned above
Update 2
I reworked my code and now the life sample is working.
My hope is that I could use the native vis.js events and use them in MyNetwork or other Components I will write.
I read about using 3rd Party DOM event in this question can't figure out to adapt it for my particular case. Because I don't know how to attach the event handler to . Is this possible to do so I can use the event in other components?
Or should I open another question for this topic?
I see several possibilities of problems here.
<Decorator/> should be defined outside of <MyNetwork /> class. Click handler should be passed as prop.
Components (espacially dynamic) shouldn't be stored in state. Just render them in render or by rendering method (called from render). Use <Node/> components with decorator prop, key prop should be used, too.
Passed props cannot be modified, you still have to copy initial data into state. State is required for updates/rerendering. You probably need to remove edge(-es) while removing node.
Create a working example (on stackblitz?) if a problem won't be resolved.
It sounds like React is re-initializing your component when you are clicking a button. Maybe someone smarter than I am can figure out why that is happening...
But since no one has commented on this yet, one way I have handled these sorts of issues is to take the state management out of the display component. You say you are passing the nodes and edges via props from a parent component. You might consider moving the addNode, removeNode, createEdge, and other methods up to the parent component so that it is maintaining the state of the node/edge structure and your display component <MyNetwork/> is only displaying what it receives as props.
Perhaps this isn't an option in your app, but I generally use Redux to remove the state management from the components all together. I find it reduces situations like this where "who should own the state" isn't always clear.

add custom prop to Component instance(not via parent) - react-data-grid

Just to provide broader context -
My eventual goal is to enhance editable Cell with "synced" icon if synced with back-end.
I went on trying to add custom prop to a specific editable Cell, to indicate syncedWithBackEnd = true/false, then have custom Formatter to conditionally add style (if sync with DB prop is true).
The issue is, I'm failing to deliver this custom prop to the Cell instance
Tried so far:
Provide callback to Formatter and call it from outside. Didn't find way to call the function on Formatter instance(attached to specific cell)
Set the prop as part of handleRowUpdated logic. Setting the custom prop but it's not reaching the Cell:
var rows = this.getRows();
...
// e.updated.customProp = "whatever" ---> fails to reach Cell
Object.assign(rows[e.rowIdx], e.updated);
this.setState({rows: rows});
Any ideas on how to achieve this? Or maybe there is some obvious way to reach the eventual goal that I've completely missed (must mention I'm new to React and pretty new to js in general).
when the actual sync happens (prior to wanting to update your custom cell) i assume it is done with a callback? if so, then your callback that did the remote ajax call to update the value could then update the this.state.rows.customProp (this.state.rows.whatever.customProp) and once the state has changed and assuming your getRows() is getting from this.state.rows then it will all automatically happen. worse case you may have to add a call to this.forceupdate() after you change the state value. this is based on something i have that sounds similar and works...
//column data inside constructor (could be a const outside component as well)
this.columnData = [
{
key: whatever,
name: whatever,
...
formatter: (props)=>(<div className={props.dependentValues.isSynced? "green-bg" : "")} onClick={(evt)=>this.clickedToggleButton} >{props.value}</div>),
getRowMetaData: (data)=>(data),
},
...
}
]
this is a callback function in my data-grid wrapper component...
//when a span
clickedToggleButton(evt, props){
//do remote ajax call we have an ajax wrapper util we created so pseudo code...
MyUtil.call(url, params, callback: (response)=>{
if(this.response.success){
let rows = this.state.rows;
let isFound = false;
for(let obj of rows){
if(obj.id==props.changedId){
obj.isSynced = true;
isFound = true;
break;
}
}
//not sure if this is needed or not...
if(isFound){ this.forceUpdate(); }
}
}
}
not sure if this helps but may get your started

Rendering a ReactJS component in memory before placing it in the DOM

I have the concept of a tile in my application.
Tiles are dynamically loaded. They can then be initialized against a DOM element using an init method, with the tile taking care of rendering itself.
features/my-tile/tile.js
import contentsComponentFactory from './components/contents/factory'
const tile = {
init,
};
// `el` is a DOM element
function init(el) {
// Render a "contents" ReactJS component (see below) to the DOM - how?
// Is the following possible?
el.appendChild(contentsComponentFactory.create({ props }).render());
}
export default tile;
A tile has a component contents which renders the tile contents to the screen.
features/my-tile/components/contents/factory.js
const factory = {
create
};
function create(options) {
const component = Object.create(React.Component.prototype);
component.props = options.props;
component.state = { message: 'This is a tile' };
component.render = function() {
return <div>{this.state.message}</div>;
};
return component;
}
export default factory;
In AngularJS, in init I would render the contents in memory and insert the result into the DOM. Can I do this in ReactJS?
I am new to ReactJS and so I may be completely misunderstanding something.
You should be able to utilize React.createElement to create the element in memory, and then ReactDOM.render() in order to insert it into the DOM.
const element = React.createElement('div', null, 'Hello, World!');
ReactDOM.render(element, document.getElementById('content'));
http://codepen.io/mikechabot/pen/PGXwxa?editors=1010
However, createElement doesn't return a native DOM element, but rather an instance of ReactElement. Not sure if this suits your needs.
This seems a pretty complicated way to do things in AngularJS, maybe you should rethink your design?
It's even worse in React, are you intending on bypassing ReactDOM and inserting it manually?
I'd recommend at least going through the React tutorial before you attempt this.

How can I access child components values from a parent component when they are added dynamically?

Current Working Example
I am creating a search form that has a varying number of input elements based on the users selection from a select box.
I have broken this up into three components, a wrapper called SearchContainer, a select box called SearchSelect, and the inputs within components called SearchWithTwo and SearchWithOne just for the sake of the example.
App
└─SearchContainer Form
│ SearchSelect
│ ... any one of the multiple search inputs (SearchWithOne, SearchWithTwo)
When a user changes the value of the select box the related component which contains the inputs is loaded. The component could have anywhere from one to ten inputs. All the examples I've seen mention using ref which would be great if my inputs weren't changing.
I currently have it working by using the following in the onSubmit handler for SearchContainer
handleSubmit: function(e) {
e.preventDefault();
var form = this.getDOMNode();
[].forEach.call(form.elements, function(e){
// get the values
});
// submit the values to get results.
}
However this doesn't feel like the proper way to be doing this. Is there a better recommended way to iterate through the children components and read their state? Or can I somehow pass the children into the parent state and get the values that way?
I think I have a solution in the form of a fork of your fiddle, and I'll cover the main ideas below.
First, I'm no React expert, but I like the idea of it, and I know it's gaining popularity so I want to learn more. What I don't know is the right way to use composition or inheritance to reduce the code duplication shown below.
Basically, my idea is to add a method to each search class that exposes its state to calling classes. This is implemented here as a very simple function inside the createClass call:
getData: function() {
return this.state;
},
It's so simple, there has to be a way to create a base class or mixin class with this method and then inherit/compose over it with other classes. I'm just not sure how. For now, you can just copy-paste these lines wherever it makes sense to expose a component's state.
Keep in mind, this is anathema to the Flux architecture everyone loves. In Flux, state always comes from a source object which lives outside the React components.
Anyway, abandoning larger architecture concerns for now, you can just grab that state variable by calling getData in the handleSubmit method. No DOM traversal required:
handleSubmit: function(e) {
e.preventDefault();
var form = this.getDOMNode(),
fd = new FormData(form);
var submitData = this.state.SearchBox.getData();
// submit the values to get results.
},

Categories

Resources