AGGrid React Custom Header Component - Updating state - reactjs

I've taken over a React project and I'm fairly new to the library. I'm attempting to implement AG-Grid and what I think is a relatively simple custom header that renders the column name and an icon. When the icon is clicked, I need the icon to change and update the state of the custom header components parent.
My issue is that the click event works once and updates the state in the parent, but the state doesn't propagate back down to the custom header component and therefore the icon doesn't switch. Not sure if this is an AG Grid issue or a React issue.
My Custom Header Component
const WeightHeaderComponent = (params) => {
const handleClick = () => {
params.weightModeCallback()
}
return (
<span onClick={handleClick} style={{cursor: 'pointer'}}>
Weight<i className={`${params.weightModeIsPercent ? 'fa fa-percent cn-blue' : 'fa fa-tally cn-blue'}`} style={{marginLeft: '3px'}}></i>
</span>
);
};
The AG Grid column def using the custom header component:
const [columnDefs] = useState([
...
{
headerName: 'Weight',
field: 'weight',
type: 'numericColumn',
headerTooltip:
'The weight of an assignment category determines the impact an assignment will have over your overall grade.',
width: 100,
defaultMinWidth: 100,
headerComponentParams: { weightModeIsPercent: weightModeIsPercent, weightModeCallback: updateWeightMode },
headerComponent: WeightHeaderComponent
},
...
])
The relevant state hook:
const [weightModeIsPercent, setWeightModeIsPercent] = useState(true);
And finally my callback function in the parent
function updateWeightMode () {
setWeightModeIsPercent(!weightModeIsPercent);
}

After an hour of so of doc reading and head-desk beating I figured it out. I was attempting to do this in a very roundabout way when AG Grid offers a nice little api which happens to have some functions for exactly this type of thing: updating your columnDefs.
Specifically: setColumnDefs(newDefs).
My code accepts an updated parameter with which I update the columnDefs for the column I want to update.
function updateWeightMode (updatedWeightModeBool) {
const newDefs = columnDefs.map((def) => {
if (def.field === 'weight') {
return {
...def,
headerComponentParams: {
weightModeIsPercent: updatedWeightModeBool,
weightModeCallback: updateWeightMode
}
};
}
return def;
});
weightsGridRef.current.api.setColumnDefs(newDefs);
}
So, this wasn't really an issue with state (though I'm still updating the parents state for other uses).

Try using
api.refreshHeader();

Related

How can I wrap a js chart libray inside of a React function component?

I have this js code that I want to create a Reactjs function component for:
const myChart = LightweightCharts.createChart(document.getElementById('myChart'), {
width: 300,
height: 600,
localization: {
dateFormat: 'yyyy-MM-dd',
locale: 'en-US',
},
timeScale: {
//visible: true,
timeVisible: true,
secondsVisible: true,
},
});
const candlestickSeriesMyChart = myChart.addCandlestickSeries();
connection.onmessage = function (event) {
// ....
candlestickSeriesMyChart.update({ time: tickTimeMinutes, open: json.minutes.o, high: json.minutes.h, low: json.minutes.l, close: json.minutes.p });
}
<div id="myChart"></div>
There is a websocket connection that will keep sending messages, and for each message I call .update(...) to update my chart.
How can I wrap this inside of a Reactjs function component, what thing I am not sure of is that I don't want to re-render my component each time a websocket message is received to update my chart.
How do you handle this scenerio with Reactjs?
Is that was a Ref is for? I have seen this in another codebase but unsure of its use case.
export function MyChart(props: ChartProps) {
return (
<div className='chart'>
<div id="myChart"></div>
</div>
);
}
My App will contain this component, and App.tsx will also make the websocket connection so I will need to pass MyChart a message each time it is received, and make sure it doesn't re-render the component each time...
<MyChart ... />
Yeah you'd want to use a ref to get access to the DOM element, to pass to your lib.
const MyChart = ({ update }) => {
const ref = useRef()
useEffect(() => {
const myChart = LightweightCharts.createChart(ref.current);
const series = myChart.addCandlestickSeries();
return () => {
// cleanup third party lib
}
}, [])
return <div>
<div ref={ref}></div>
</div>
}
As for updating that chart without a re-render there's probably a lot of ways to go about this.
We could add a callback function that executes in the child after lib init, and leave the method of doing something with this instance up to the parent:
const MyApp = () => {
const myRef = useRef()
useEffect(() => {
callChild = () => myRef.current && myRef.current.update(...)
}, [])
return <MyChart initCallback={(instance) => {
// do something with the instance
// maybe store it in another ref or state
myRef.current = instance
// or we could bind our message handlers directly in here
}} />
}
But storing data from the child in the parent like this kind of has a code smell.
I'm not sure what pattern is best here. I would consider passing the socket down to the Chart component, or if we want reusability of a core Chart component, creating a SocketChart wrapper. That SocketChart could have message transformer props that helps adjust it per context.
PS: I see that chart lib has a React version.

How to pass row data to a child component created into a format function

I have a react-bootstrap table with column defined like this:
const columns = [
...
{
dataField: "d",
text: "Actions",
formatter: actionFormatter,
headerAlign: "center",
},
];
The formatter function looks like this:
const actionFormatter = (cell, row) => {
return (
<div className="action-cell text-center">
<Tooltip title="View file">
<span className="svg-icon svg-icon-xl" onClick={setSelectRow}>
<SVG
src={toAbsoluteUrl("/media/svg/icons/General/Binocular.svg")}
/>
</span>
</Tooltip>
<Tooltip title="Download file">
<span className="svg-icon svg-icon-xl">
{/* Rename file on download */}
<SVG
src={toAbsoluteUrl("/media/svg/icons/Files/Download.svg")}
onClick={setSelectRow}
/>
</span>
</Tooltip>
</div>
);
};
The first part is used to show some content in an other component. I needed to get data to the parent component. But the problem is that the formatter is created once and only once at the component creation, it has no idea about any event or changes happening to row. I already struggled to do this but finished by getting it done by doing something like this:
const rowEvents = {
onClick: (e, row, rowIndex) => {
setSelectedRow(row);
setSelectedRowIsReady(true);
},
};
So, this method allow me to get the selected row by a click event on the row,
then,
const setSelectRow = useCallback(() => {
if (selectedRowIsReady) {
props.onSelectSetting(selectedRow);
}
}, [props, selectedRow, selectedRowIsReady]);
useEffect(() => {
setSelectRow();
}, [setSelectRow]);
the setSelectRow function allow me to send my row to the parent components, I had to put it in a useEffect hook because the onClick function was called before the rowEvents.
So... not very clean but at least working as expected.
Now, what I want do to is replace my formatter to look like this:
<div className="action-cell text-center">
<Tooltip title="View file">
<span className="svg-icon svg-icon-xl" onClick={setSelectRow}>
<SVG
src={toAbsoluteUrl("/media/svg/icons/General/Binocular.svg")}
/>
</span>
</Tooltip>
<SystemFileDownload row={selectedRow} />
</div>
However I do not know how to get my row into my child component because, when onClick is called, the state isn't already set. I thought about setting my selected row into my redux store but it feels kind of overkill for what I am trying to achieve.
How could I simply pass my current row to my child component ?
I tried to add my state to formatExtraData like this:
{
dataField: "d",
text: "Actions",
formatter: actionFormatter,
headerAlign: "center",
formatExtraData: selectedRow,
},
and then use selectedRow onto my props, but my child component got the row that was click on the previous click and not on the current click.
So I tried to set my row into my redux. Obviously I got the same problem, the onclick function is triggered before the rowEvents function.
So I added this to my child component:
useEffect(
(
downloadFileHandler = () => {
console.log("Download");
// Will get the data from the API
const fake_data = '{"hello": "no"}';
console.log(selectedConf);
console.log(systemName);
const fileName = ``;
}
) => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
downloadFileHandler();
},
[selectedConf]
);
Where I get selectedConf from my redux store, and it is set on click on a row.
But then, when I click on my download button, the useEffect hook get triggered as many time as my child component exists on the page, even if it's not part of the BootstrapTable I am working on, so not good, I do not know how to avoid this.
I feel like I am missing something obvious because my use case is like very common and simple and I have to use hooks and redux to achieve it instead of a simple props and it isn't even working.

React Checkbox Form State is Delayed

I have a form I am mapping a list of checkbox inputs to using React. The mapping function receives a list of JSON objects and maps them to inputs as follows:
const listCustomBenefits = props.customBenefits.map(benefit => {
return(
<div className="flex">
<input onChange={props.handleCustomBenefitsChange} id={benefit.id} key={benefit.id}
className="my-auto" type="checkbox" checked={benefit.included}></input>
<p className="my-auto px-2">{benefit.benefit}</p>
</div>
)
})
props.customBenefit is formatted as followed:
const customBenefit = useState([
{
benefit: "first benefit description",
id: 1,
included: false,
name: "First Benefit",
},
{
benefit: "second benefit description",
id: 2,
included: false,
name: "Second Benefit",
},
]);
listCustomBenefits is then returned in the component's return JSX. When a checkbox is clicked, handleCustomBenefitsChange updates the state of custom benefits as follows:
const handleCustomBenefitsChange = (event) =>{
event.preventDefault()
const newCustomBenefits = customBenefits.map(benefit =>{
if(benefit.id == event.target.id){
const newCheck = [event.target.checked][0]
return({...benefit, included: newCheck})
}
else{
return benefit
}
})
setCustomBenefits(newCustomBenefits)
}
The function properly updates the state of customBenefits, but the checkbox display is always one event behind (i.e. you have to click the checkbox twice to see the desired change).
I have tried setting state with props on the local component as well (instead of using props directly) but can't seem to get the component to update on the initial click. Can anyone help me resolve this?
I didn't dig into exactly why, but I think it's something to do with the newCheck being out of date. Instead of relying on the dom state we can just flip benefit.included and pass around the id instead of grabbing it from the event target.
So our checkbox would look like this
<input
onChange={() => handleCustomBenefitsChange(benefit.id)}
...
/>
and our handler changes to accept the ID instead of the event, and makes the change based off of it's own state rather than the dom
const handleCustomBenefitsChange = id => {
const newCustomBenefits = customBenefits.map(benefit => {
if(benefit.id == id){
return({...benefit, included: !benefit.included})
} else {
return benefit;
}
});
setCustomBenefits(newCustomBenefits);
};

How do I trigger the change event on a react-select component with react-testing-library?

Given that I can't test internals directly with react-testing-library, how would I go about testing a component that uses react-select? For instance, if I have a conditional render based on the value of the react-select, which doesn't render a traditional <select/>, can I still trigger the change?
import React, { useState } from "react";
import Select from "react-select";
const options = [
{ value: "First", label: "First" },
{ value: "Second", label: "Second" },
{ value: "Third", label: "Third" },
];
function TestApp() {
const [option, setOption] = useState(null);
return (
<div>
<label htmlFor="option-select">Select Option</label>
<Select
value={option}
options={options}
onChange={option => setOption(option)}
/>
{option && <div>{option.label}</div>}
</div>
);
}
export default TestApp;
I'm not even sure what I should query for. Is it the hidden input?
My team has a test utility in our project that lets us select an item easily after spending too much time trying to figure out how to do this properly. Sharing it here to hopefully help others.
This doesn't rely on any React Select internals or mocking but does require you to have set up a <label> which has a for linking to the React Select input. It uses the label to select a given choice value just like a user would on the real page.
const KEY_DOWN = 40
// Select an item from a React Select dropdown given a label and
// choice label you wish to pick.
export async function selectItem(
container: HTMLElement,
label: string,
choice: string
): Promise<void> {
// Focus and enable the dropdown of options.
fireEvent.focus(getByLabelText(container, label))
fireEvent.keyDown(getByLabelText(container, label), {
keyCode: KEY_DOWN,
})
// Wait for the dropdown of options to be drawn.
await findByText(container, choice)
// Select the item we care about.
fireEvent.click(getByText(container, choice))
// Wait for your choice to be set as the input value.
await findByDisplayValue(container, choice)
}
It can be used like this:
it('selects an item', async () => {
const { container } = render(<MyComponent/>)
await selectItem(container, 'My label', 'value')
})
You can try the following to get it working:
Fire focus event on the ReactSelect component .react-select input element.
Fire a mouseDown event on the .react-select__control element
Fire a click on the option element that you want to select
You can add a className and classNamePrefix props with the value of "react-select" in order to specifically select the component you are trying to test.
PS: In case you are still stuck I'd encourage you to take a look at this conversation from where the above answer is borrowed - https://spectrum.chat/react-testing-library/general/testing-react-select~5857bb70-b3b9-41a7-9991-83f782377581

How to change react element (li) with onClick to be strikethrough?

I've been trying to set element to become strikethrough when I click on it, but unfortunately I couldn't, nothing happens.
var UserList = React.createClass({
getInitialState: function() {
return {
user: [],
firstName: '',
lastName: '',
createdAt: 0,
isClicked: false,
};
},
handleOnClick: function() {
var isClicked = this.state.isClicked;
var style = {textDecoration: 'none'};
if (isClicked === true) {
style = {textDecoration: 'line-through'}
}
},
render: function() {
return (
<div>
<Users user={this.state.user} onClick={this.handleOnClick}/>
</div>
);
You can do it this way:
A similar example to yours:
const TodoItem = ({item, checkHandler}) => {
const itemCheckHandler = () => {
checkHandler (item.id);
};
return (
<div>
<li
style={{
textDecoration: item.checked ? 'line-through' : 'none',
}}
onClick={itemCheckHandler}
>
{item.text}
</li>
</div>
);
};
and your checkHandler in your App.js where the state resides is like this (items bein an array of items):
checkHandler = id => {
this.setState ({
items: this.state.items.map (item => {
if (item.id === id) {
item.checked = !item.checked;
}
return item;
}),
});
};
Don't try to change the style in a click handler. You should not change the style when a user does an action but rather do it at the time of it being rendered, that's the correct approach.
Store the "strikethrough" value in a flag in the state and do it in the render function.
For example:
getInitialState: function () {
return {
...
isStrikeThrough: false,
...
}
},
onHandleClick: function () {
....
// toggle the strikethrough state
this.setState({isStrikeThrough: !this.state.isStrikeThrough});
....
},
render: function () {
return (
<div>
<User
user={this.state.user}
strikeThrough={this.state.isStrikeThrough}
onClick={this.handleOnClick}
/>
</div>
);
},
You haven't given any details about the User component, so the explanation above is based solely on what we have in the question. That said, there are a couple of ways in which this could be improved.
First, I'm assuming that you can add the strikethrough flag to the User component and render the <strike>...</strike> (or comparable CSS styles) there. That may or may not be true (ie. if the User component is a third-party component, it may be difficult to change it).
Second, the strikethrough state described above looks to me like it ought to be internal to the User component. If all you're doing is changing the markup in the User component based on a click on the User component, then the strikethrough code ought to be in the User component. And, perhaps more importantly, if the strikethrough is supposed to represent something important about the state of a user, something that should be saved as part of the user's state, then the strikethrough flag ought to be part of the user's state (and have a more informative name than isStrikeThrough).
Dodek you can see the above answers but I think you need to change the way you look and think about a react application then it helps you a lot during your coding with react. The code you provided looks like a jQuery approach to me that you directly modify the DOM element when user do an action. Even if there was no issue in your approach still your code does not apply 'line-through' style to an already checked element which you get them from backend unless user clicks on an item.
You should look at your component as a very simple actor in a movie that accepts a very small set of parameters (compared to real word) and based on this input parameters it changes the way it appears in the frame. For example lets say you have a Todo item component (Like the one #Vennessa has provided here) in a vey simple case it accepts only an item text and also whether or not the item is checked;
These parameters may come from internal state or come from props or any other resources but in the end your component is accepting these parameters and all your internal logic that determines how your component should look must only rely and work with these params.
Try this out:
function Item(props){
const [clicked, setIsClicked] = useState(false);
function handleClick(){
setIsClicked((prevValue) => {
return (!prevValue)
});
}
return <li style={{textDecoration: clicked ? "line-through": "none" }} onClick={handleClick}>
{props.text} </li>
}

Resources