How to get the selection order in ag-Grid? - reactjs

I'm using the grid api to get the selected rows by using the function getSelectedRows(). However, the list of rows seems to be unordered, i.e the items are not in the order I selected them in.
Is there another way to get the selected rows in the order they were selected?

You can keep track of the selected items yourself and make sure it's chronologically ordered by listening to selectionChanged event.
// global keyboard state, put this outside of the function body
// we need to store the current shift key state to know if user
// are selecting multiple rows
const KEYBOARD_STATE = {
isShiftPressed: false
};
document.addEventListener("keydown", (e) => {
KEYBOARD_STATE.isShiftPressed = e.key === "Shift";
});
document.addEventListener("keyup", (e) => {
KEYBOARD_STATE.isShiftPressed = false;
});
const [selection, setSelection] = React.useState([]);
const onSelectionChanged = (e) => {
const selectedNodes = e.api.getSelectedNodes();
const lastSelectedNode =
selectedNodes[0]?.selectionController?.lastSelectedNode;
// if lastSelectedNode is missing while multi-selecting,
// AgGrid will select from the first row
const selectedNodeFrom = lastSelectedNode || e.api.getRenderedNodes()[0];
const isRangeSelect = KEYBOARD_STATE.isShiftPressed;
const difference = (arr1, arr2) => arr1.filter((x) => !arr2.includes(x));
const newSelection = difference(selectedNodes, selection);
if (newSelection.length > 0) {
if (isRangeSelect) {
const isSelectBackward =
newSelection[0].rowIndex < selectedNodeFrom.rowIndex;
if (isSelectBackward) {
newSelection.reverse();
}
}
newSelection.forEach((n) => updateSelection(n));
} else {
updateSelection(); // deselect
}
};
const updateSelection = (rowNode) => {
setSelection((selections) => {
if (rowNode) {
const isSelected = rowNode.isSelected();
if (isSelected) {
return [...selections, rowNode];
} else {
return selections.filter((s) => s.id !== rowNode.id);
}
} else {
return selections.filter((n) => n.isSelected());
}
});
};
return (
<>
<pre>
{JSON.stringify(selection.map((n) => n.data.id))}
</pre>
<AgGridReact
rowSelection="multiple"
columnDefs={columnDefs}
rowMultiSelectWithClick
onSelectionChanged={onSelectionChanged}
{...}
/>
</>
);
Live Demo

Related

Update react button disabled state on redux state change

I've set up a new project with React, Redux (using toolkit). I've got a button that needs to be disabled if the user does not have enough of a particular resource. I've confirmed that the state is being updated properly and the reducers are applying to state properly, but I am unable to get the button to disable when that resource falls below the supplied price.
I've tried duplicating state from redux using a useState hook, but setting the state within canAfford() still doesn't disable the button. I'm at a bit of a loss, and feel like I'm just missing something about redux state and rendering.
Here's the button component I'm working with:
function BuyBtn({ technology, label, resourceType, price, requirements = []}: IBuyBtn) {
const units = useSelector((state: any) => state.units);
const tech = useSelector((state: any) => state.tech);
const resources = useSelector((state: any) => state.resources);
const dispatch = useDispatch();
let disabled = false;
let unlocked = true;
useEffect(() => {
disabled = !canAfford()
}, [resources])
const canAfford = (): boolean => {
console.log('Units:', units);
console.log("Checking affordability");
if (resourceType.length != price.length) {
throw `BuyBtn Error: price length is ${price.length} but resource length is ${resourceType.length}.`;
}
resourceType.forEach((res, i) => {
const resPrice = price[i];
if (resources[res] < resPrice) {
return false;
}
});
return true;
};
const meetsRequirements = (): boolean => {
if (requirements.length === 0) {
return true;
}
requirements.forEach((req) => {
if (!tech[req]) {
return false;
}
});
return true;
};
const buyThing = () => {
if (canAfford() && meetsRequirements()) {
resourceType.forEach((res, i) => {
const resPrice = price[i];
dispatch(SubtractResource(res, resPrice));
});
dispatch(UnlockTech(technology, true))
}
};
if (meetsRequirements() && canAfford()) {
return (
<button onClick={buyThing} disabled={disabled}>{label}</button>
);
} else {
return null;
}
}
export default BuyBtn;
Instead of using disabled as variable make it State which will trigger re-render:
function BuyBtn({ technology, label, resourceType, price, requirements = []}: IBuyBtn) {
const units = useSelector((state: any) => state.units);
const tech = useSelector((state: any) => state.tech);
const resources = useSelector((state: any) => state.resources);
const dispatch = useDispatch();
const [disabled, setDisabled] = React.useState(false);
let unlocked = true;
const canAfford = (): boolean => {
console.log('Units:', units);
console.log("Checking affordability");
if (resourceType.length != price.length) {
throw `BuyBtn Error: price length is ${price.length} but resource length is ${resourceType.length}.`;
}
let isAffordable = true
resourceType.forEach((res, i) => {
const resPrice = price[i];
if (resources[res] < resPrice) {
isAffordable = false;
}
});
return isAffordable;
};
useEffect(async() => {
const value = await canAfford();
setDisabled(!value);
}, [resources])
const meetsRequirements = (): boolean => {
if (requirements.length === 0) {
return true;
}
let isMeetingRequirements = true;
requirements.forEach((req) => {
if (!tech[req]) {
isMeetingRequirements = false;
}
});
return isMeetingRequirements;
};
const buyThing = () => {
if (canAfford() && meetsRequirements()) {
resourceType.forEach((res, i) => {
const resPrice = price[i];
dispatch(SubtractResource(res, resPrice));
});
dispatch(UnlockTech(technology, true))
}
};
if (meetsRequirements() && canAfford()) {
return (
<button onClick={buyThing} disabled={disabled}>{label}</button>
);
} else {
return null;
}
}
export default BuyBtn;

My useState hook is not updating itself and when i am trying to get data using filter, its not working

When I am trying to get data from an array using filter and find, it's not getting filtered also the _ids are the same when I cross-checked the array, also useState is also not updating
1. How should I filter one element from an array, Am I doing this right?
2. useState is not working, not updating data
I am getting every data from context (c1)
sd is returning array of single object, so to get one first index I am returning sd[0]
const ReadTemplate = (props) => {
const c1 = useContext(PostsContext);
const [first, myData] = useState({});
const first_load_func = () => {
const id = props.match.params.id;
const sd = c1.data.filter((c1) => id === c1._id);
const business_props = c1.business_data.filter((c1) => id === c1._id);
const startups_props = c1.startups_data.filter((c1) => id === c1._id);
const tech_props = c1.tech_data.filter((c1) => id === c1._id);
const sports_props = c1.sports_data.filter((c1) => id === c1._id);
if (sd) {
return sd[0];
} else if (business_props) {
return business_props[0];
} else if (startups_props) {
return startups_props[0];
} else if (tech_props) {
return tech_props[0];
} else if (sports_props) {
return sports_props[0];
} else {
return <MyAlert />;
}
};
const func = (data) => {
if (data) {
setTimeout(() => {
myData(data);
}, 1000);
console.log('ye first hai');
console.log(first._id);
console.log('ye data hai');
console.log(data);
} else {
console.log('No');
}
};
useEffect(() => {
first_load_func();
func(first_load_func());
}, [first]);
return (
<>
<PostDesign props={first} />
</>
);
};
export default ReadTemplate;
My guess from your code is that you should assign the filtered data when the component is rendered, not when first changes:
useEffect(() => {
func(first_load_func());
}, []);
It may be useful to convert ids toString() before comparing them:
const sd = c1.data.filter((c1) => id.toString() === c1._id.toString());

State variable hook does not increment within closure

codesandbox.io/s/github/Tmcerlean/battleship
I am developing a simple board game and need to increment a state variable when a player clicks on a cell with a valid move.
The functionality for validating the move and making the move is all in place, however, I am having difficulty updating the state within the event listener.
I can see that the state is being updated when observed from a useEffect hook, but not when viewed from within the function (even following successive calls).
I have done some reading and believe it could have something to do with having a stale closure, but I am not certain.
My approach to solve this issue was to remove and then re-add the click event listener following every click by the user.
My assumption was that this would cause the correct (newly incremented) state variable to be picked up. Unfortunately, this does not appear to be the case and within the event listener function, the variable is never incremented from 0.
I initialise the state variable here:
const [placedShips, setPlacedShips] = useState(0);
Next, a click event listener is applied to each cell within the gameboard:
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
You will see that the setPlacedships state variable is incremented here and there is a console log to report its value.
I am aware that the useState hook is asynchronous and so console.log will show 0 for the first time it is called. Consequently, I have a useEffect hook deployed outside of the function which also contains a console.log to report the changed value of setPlacedShips:
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips)
}, [placedShips])
After every click the placedShips variable is incremented by 1 and then two functions are run:
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
which is immediately followed by the original setEventListeners function:
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
As mentioned above, the issue is that the console log within the setEventListeners function constantly remains at 0, while the console log within the useEffect hook increments as expected.
For reference, here is the full component I am working on currently:
import React, { useEffect, useState, useLayoutEffect } from "react";
import gameboardFactory from "../../factories/gameboardFactory";
import Table from "../Reusable/Table";
import "./GameboardSetup.css";
// -----------------------------------------------
//
// Desc: Gameboard setup phase of game
//
// -----------------------------------------------
let playerGameboard = gameboardFactory();
const GameboardSetup = () => {
const [humanSetupGrid, setHumanSetupGrid] = useState([]);
const [ships, _setShips] = useState([
{
name: "carrier",
length: 5,
direction: "horizontal",
},
{
name: "battleship",
length: 4,
direction: "horizontal",
},
{
name: "cruiser",
length: 3,
direction: "horizontal",
},
{
name: "submarine",
length: 3,
direction: "horizontal",
},
{
name: "destroyer",
length: 2,
direction: "horizontal",
},
]);
const [placedShips, setPlacedShips] = useState(0);
const createGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(0);
}
};
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} />;
});
setHumanSetupGrid(result);
};
const setUpPlayerGrid = () => {
// createGrid('grid');
createUiGrid();
};
const currentShip = () => {
return ships[placedShips];
};
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
cell.addEventListener("mouseover", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
}
});
cell.addEventListener("mouseleave", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
}
});
});
};
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
useEffect(() => {
setUpPlayerGrid();
// setUpComputerGrid();
}, []);
useEffect(() => {
console.log(humanSetupGrid);
}, [humanSetupGrid]);
// Re-render the component to enable event listeners to be added to generated grid
useLayoutEffect(() => {
setEventListeners();
});
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips);
}, [placedShips]);
return (
<div className="setup-container">
<div className="setup-information">
<p className="setup-information__p">Add your ships!</p>
<button
className="setup-information__btn"
onClick={() => console.log(placedShips)}
>
Rotate
</button>
</div>
<div className="setup-grid">
<Table grid={humanSetupGrid} />
</div>
</div>
);
};
export default GameboardSetup;
I am quite confused what is happening here and have been stuck on this problem for a couple of days now - if anybody has any suggestions then they would be highly appreciated!
Thank you.
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
The above code does not remove any event listeners, which is probably the reason why 0 is still being logged. You pass a new anonymous function to removeEventListener. Since the function is just created it will never remove any event listeners, because it is not registered as an event listener.
Two different functions that do the same are not equal, which is why the event listener is not removed.
const a = (e) => clickListener(e); // passed to addEventListener
const b = (e) => clickListener(e); // passed to removeEventListener
console.log(a == b); //=> false
To add and remove events you cannot use anonymous functions. You either have to use named functions, or store the function in a variable. Then register and remove the event listener using the function name or variable.
Since you only forward the event to the clickListener you can simply replace your event handler registration with:
cell.addEventListener("click", clickListener);
Then remove it using:
cell.removeEventListener("click", clickListener);
Note that this scenario could've been avoided if you passed your event handlers using a more React approach. Instead of using cell.addEventHandler(...) you could've passed the event on creation of this element. eg. <div className='cell' id={counter} onClick={clickListener} />
When working with React you should try to not manipulate the DOM manually. React Components have Synthetic Events, which means that you don't need to add event listeners the vanilla way.
Just add each synthetic event to the cell component with its corresponding handler.
You can do it in the createUiGrid function:
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} onClick={onClickHandler} onMouseOut={onMouseOutHandler} onMouseOver={onMouseOverHandler} />;
});
setHumanSetupGrid(result);
};
And then just move the code you did on vanilla to each corresponding handler (be sure to remove all listener manipulation before testing).

useEffect() triggers components re-render in one function but not in the other one. Both function DO change state. What am I missing?

It must be something really silly I do wrong here. useEffect() works perfectly with MonthModificatorHandler but not re-render when using dayClick. When dayclick was only adding days re-render worked properly. After adding logic to remove days already in state re-rendering stopped. I can call saveChanges and loadTimeline to fix functionality but if you click few days in a row asynchronous call leads to unexpected results. Thanks for your time.
export default function DatePicker(props) {
const classes = useStyles();
const theme = useTheme();
const [monthModificator, setMonthModificator] = React.useState(0);
const [monthMatrix, setMonthMatrix] = React.useState([]);
const [selectedDates, setSelectedDates] = React.useState([]);
const MonthModificatorHandler = value => {
setMonthModificator(monthModificator + value);
};
const dayClick = day => {
let data = selectedDates;
let addDay = true;
if (data.length === 0) {
data.push(day);
} else {
data.map((date, index) => {
if (day.equals(date)) {
data.splice(index, 1);
addDay = false;
}
});
if (addDay) {
data.push(day);
}
}
setSelectedDates(data);
// saveChanges();
// loadTimeline();
};
let now = DateTime.local().plus({ months: monthModificator });
let firstDayOfFirstWeek = now.startOf("month").startOf("week");
let lastDayOfLasttWeek = now.endOf("month").endOf("week");
let monthToDisplay = Interval.fromDateTimes(
firstDayOfFirstWeek,
lastDayOfLasttWeek
);
function loadTimeline() {
axios.get(`/timeline`).then(response => {
let selectedDays = [];
response.data.map(date => {
selectedDays.push(DateTime.fromISO(date));
});
setSelectedDates(selectedDays);
});
}
useEffect(() => {
let load = true;
if (load) {
loadTimeline();
load = false;
}
var matrix = [];
for (let v = 0; v < monthToDisplay.length("day"); v++) {
matrix.push(firstDayOfFirstWeek.plus({ day: v }));
}
setMonthMatrix(matrix);
}, [selectedDates, monthModificator]);
function saveChanges() {
let arrayOfDataObjects = selectedDates;
let arrayOfDataStrings = arrayOfDataObjects.map(singleDataObject => {
return (
"," +
JSON.stringify(singleDataObject.toISODate()).replaceAll('"', "") // extra quotes removed
);
});
axios.post(`/timeline`, {
timeline: arrayOfDataStrings
});
}
return (
<Grid container justify="space-around">
<Button onClick={() => MonthModificatorHandler(1)}>+</Button>
<Button onClick={() => MonthModificatorHandler(-1)}>-</Button>
<Card className={classes.root}>
{monthMatrix.map((day, index) => {
let color = "secondary";
selectedDates.map(workingDay => {
if (day.equals(workingDay)) {
color = "primary";
}
});
return (
<Button
color={color}
variant="contained"
onClick={() => dayClick(day)}
className={classes.days}
key={index}
>
{day.day}
</Button>
);
})}
</Card>
<Button onClick={() => saveChanges()}>Save Changes</Button>
<Button onClick={() => loadTimeline()}>Update Changes</Button>
</Grid>
);
}
Maybe the problem is that you compute new state from previous state. It should be done with callback https://reactjs.org/docs/hooks-reference.html#functional-updates
Try something like
const dayClick = day => setSelectedDates((_data) => {
let data =[..._data];
let addDay = true;
if (data.length === 0) {
data.push(day);
} else {
data.map((date, index) => {
if (day.equals(date)) {
data.splice(index, 1);
addDay = false;
}
});
if (addDay) {
data.push(day);
}
}
return data
})
Answered by Kostya Tresko, thank you. On top of that, another mistake was in the hook itself. The way I loaded data caused re rending loop.
if (load) {
loadTimeline();
load = false;
}
DO NOT DO THAT

Select item matched in the middle in ReactBootstrapTypeahead

In my ReactBootstrapTypeahead component, I can click any option to select it, but using Tab will only select the first option if it matches at the start of the string. If the first option is matched in the middle, hitting Tab merely moves the focus without selecting an option. It does work if you first use the arrow keys to highlight the option.
Each option contains an id, make, and model, and I combine the last two into name (e.g. 'Toyota Prius') for use by labelKey. Everything works except selecting the first item without highlighting it using the arrow keys.
<AsyncTypeahead
id="search"
selectHintOnEnter
labelKey="name"
filterBy={startsWith}
renderMenu={renderMenu}
options={results}
/>
I'm using a custom renderMenu function to group options...
const renderMenu = (results, menuProps) => {
const items = [];
let makesHeader, lastMake, idx = 0;
results.forEach(result => {
const { id, make, model } = result;
if (!make) {
// skip "click to load more..." text
items.push(
<Menu.Header key="more-header">
More Results Hidden…
</Menu.Header>
);
}
else if (!model) {
if (!makesHeader) {
items.push(
<Menu.Header key="makes-header">
Makes
</Menu.Header>
);
makesHeader = true;
}
items.push(
<MenuItem key={id} option={result} position={idx}>
<WordHighlighter search={menuProps.text}>
{make}
</WordHighlighter>
</MenuItem>
);
}
else {
if (make !== lastMake) {
items.push(
<Menu.Header key={`${make}-header`}>
{make}
</Menu.Header>
);
lastMake = make;
}
items.push(
<MenuItem key={id} option={result} position={idx}>
<WordHighlighter search={menuProps.text}>
{model}
</WordHighlighter>
</MenuItem>
);
}
idx++;
});
return <Menu {...menuProps}>{items}</Menu>;
};
...and highlight only matches at the start of any word.
const WordHighlighter = props => {
const search = props.search.toLowerCase(),
len = search.length,
parts = [];
let count = 0;
props.children.split(' ').forEach(word => {
if (word.toLowerCase().startsWith(search)) {
parts.push(<mark className="rbt-highlight-text"
key={++count}>{word.substr(0, len)}</mark>);
parts.push(<span key={++count}>{word.substr(len) + ' '}</span>);
}
else {
parts.push(<span key={++count}>{word + ' '}</span>);
}
});
return <span>{parts}</span>;
};
I solved this by overriding the Typeahead's onKeyDown callback with my own.
const typeahead = useRef();
const onChange = useCallback(([ result ]) => {
typeahead.current.getInstance().clear();
typeahead.current.getInstance().blur();
history.push(`/cars/${result.slug}`);
}, [ typeahead ]);
useEffect(() => {
const instance = typeahead.current.getInstance(),
original = instance._handleKeyDown;
instance._handleKeyDown = (e, results, isMenuDown) => {
if (results && results.length && (e.key === 'Enter' || e.key === 'Tab')) {
e.preventDefault();
e.stopPropagation();
onChange(results);
}
else {
return original(e, results, isMenuDown);
}
};
}, [ typeahead, onChange ]);

Resources