How to render loader on button clicked? - reactjs

I pass an array of actions in child component. Then in child component I render three buttons. Each button get its own handler. Also I pass loader to child component.
I expect to get the following: on "Button 1" click the first button should become "Button 1 click" and no changes to other buttons.
What I actually get: on "Button 1" click. All buttons get "click" text.
How can I fix that? Codesandbox https://codesandbox.io/s/wispy-river-m2zgb?file=/src/App.tsx
interface IButtonBlockProps {
actions: {
tool: string;
onClick: () => void;
}[];
loader: any;
}
enum Loader {
Button1 = "button 1"
}
const ButtonBlock: React.FC<IButtonBlockProps> = ({ actions, loader }) => {
return (
<div>
{actions.map((item, idx) => (
<button key={idx} onClick={item.onClick}>
{item.tool}
{loader === Loader.Button1 && "Clicked"}
</button>
))}
</div>
);
};
const App: React.FC = () => {
const [loader, setLoader] = useState<Loader | null>(null);
const handleClick = () => {
console.log("on Button 1 click");
setLoader(Loader.Button1);
};
const actions = [
{
tool: "Button 1",
onClick: () => handleClick()
},
{
tool: "Button 2",
onClick: () => console.log("Button 2")
},
{
tool: "Button 3",
onClick: () => console.log("Button 3")
}
];
return (
<div className="App">
<ButtonBlock actions={actions} loader={loader} />
</div>
);
};

You can add one more check to your button if its Button1 or not something like below:
<button key={idx} onClick={item.onClick}>
{item.tool}
{loader === Loader.Button1 && item.tool === "Button 1" && "Clicked"}
</button>

What you could do instead is have an array of loading indexes, and whenever mapping the actions, if the loading array contains the index of the action, you can show clicked or loading or whatever.
This is assuming there would be something that after the button is no longer loading (like in the case of an API call completing) you would set the button to not loading by removing that index from the array.
Here is a modified version of your code sandbox that achieves this:
interface IButtonBlockProps {
actions: {
index: number;
tool: string;
onClick: () => void;
}[];
loader: any;
}
const ButtonBlock: React.FC<IButtonBlockProps> = ({ actions, loader }) => {
return (
<div>
{actions.map((item, idx) => (
<button key={idx} onClick={item.onClick}>
{item.tool}
{loader.includes(item.index) && `Clicked`}
</button>
))}
</div>
);
};
const App: React.FC = () => {
const [loadingButtons, setLoadingButtons] = useState<number[]>([]);
console.log(loadingButtons);
const handleClickButton1 = useCallback(() => {
console.log("on Button 3 click");
setLoadingButtons([...loadingButtons, 0]);
setTimeout(() => {
setLoadingButtons([...loadingButtons.filter((x) => x !== 0)]);
}, 5000);
}, [loadingButtons]);
const handleClickButton2 = useCallback(() => {
console.log("on Button 2 click");
setLoadingButtons([...loadingButtons, 1]);
setTimeout(() => {
setLoadingButtons([...loadingButtons.filter((x) => x !== 1)]);
}, 5000);
}, [loadingButtons]);
const handleClickButton3 = useCallback(() => {
console.log("on Button 3 click");
setLoadingButtons([...loadingButtons, 2]);
setTimeout(() => {
setLoadingButtons([...loadingButtons.filter((x) => x !== 2)]);
}, 5000);
}, [loadingButtons]);
const actions = [
{
index: 0,
tool: "Button 1",
onClick: () => handleClickButton1()
},
{
index: 1,
tool: "Button 2",
onClick: () => handleClickButton2()
},
{
index: 2,
tool: "Button 3",
onClick: () => handleClickButton3()
}
];
return (
<div className="App">
<ButtonBlock actions={actions} loader={loadingButtons} />
</div>
);
};

Related

Click and insert object in one array to another empty array using react hooks

I have array of objects (items) with button clickHandler function. When you click on button, it should add that object to new array named ‘myNewArray’. Please help me to achieve this. I added demo object inside array ‘myNewArray’.
Explaination: If i click on category button '1-furniture', that object will added to new array named 'myNewArray'
import React, { useState } from "react";
const App = () => {
const [items, setItems] = useState([
{ name: "Furniture", categoryKey: 1 },
{ name: "Shoes", categoryKey: 2 },
{ name: "Electronics", categoryKey: 3 },
{ name: "Clothes", categoryKey: 4 },
{ name: "Grocery", categoryKey: 5 },
]);
const [myNewArray, setMyNewArray] = useState([{ name: "demo-item", categoryKey: 100 }]);
const clickHandler = (categoryKey: any) => {
console.log(categoryKey);
};
return (
<div>
{items.map((item) => (
<button onClick={() => clickHandler(item.categoryKey)} key={item.categoryKey}>
{item.categoryKey}-{item.name}
</button>
))}
<h4>New array after clicking on item from above array[items]</h4>
{myNewArray.map((item) => (
<button onClick={() => clickHandler(item.categoryKey)} key={item.categoryKey}>
{item.categoryKey}-{item.name}
</button>
))}
</div>
);
};
export default App;
just use set method in the useState
const clickHandler = (item: any) => {
setMyNewArray(prev => [...prev, {name:item.name,categoryKey: item.categoryKey}])
};
and pass item in the click
onClick={() => clickHandler(item)}
Here's the working solution:
import React, { useState, useEffect } from "react";
const App = () => {
const [items, setItems] = useState([
{ name: "Furniture", categoryKey: 1 },
{ name: "Shoes", categoryKey: 2 },
{ name: "Electronics", categoryKey: 3 },
{ name: "Clothes", categoryKey: 4 },
{ name: "Grocery", categoryKey: 5 }
]);
const [myNewArray, setMyNewArray] = useState([
{ name: "demo-item", categoryKey: 100 }
]);
useEffect(() => {
console.log(myNewArray);
}, [myNewArray]);
const clickHandler = (item) => {
setMyNewArray([...myNewArray, item]);
};
return (
<div>
{items.map((item) => (
<button onClick={() => clickHandler(item)} key={item.categoryKey}>
{item.categoryKey}-{item.name}
</button>
))}
<h4>New array after clicking on item from above array[items]</h4>
{myNewArray.map((item, i) => (
<button key={i}>
{item.categoryKey}-{item.name}
</button>
))}
</div>
);
};
export default App;
The live demo is here: https://codesandbox.io/s/determined-solomon-phyy9u?file=/src/App.js:0-1163
You can have a look at the console to check the myNewArray updates.
You could also do like this.
if Item is with the matching category Key. Then, it's Update the myNewArray state with the new item
const clickHandler = (categoryKey: any) => {
const item = items.find((i) => i.categoryKey === categoryKey);
setMyNewArray([...myNewArray, item]);
};
Here is Codesandbox

ag-grid keyboard navigation for frameworkComponents is not working

I'm trying to enable the keyboard action for my ag-grid.
I have few icons in my ag-grid I'm trying to click the icons through keyboard navigation.
to trigger the event via keyboard navigation I used onCellKeyPress where the user clicks enter key I'll trigger the event.
const onCellKeyPress = useCallback(
(keyPressEvent) => {
if (!isNil(keyPressEvent)) {
const {
event: { key },
colDef: { cellRenderer },
data: { indexId },
} = keyPressEvent;
if (key === ENTER_KEY) {
if (cellRenderer === 'deleteButtonSchemaRenderer') {
deleteRef?.current?.handleClickDeleteRow();
}
}
}
}
,[deleteRef?.current?.handleClickDeleteRow])
in framework component i just import my delete icon react component where i'm just doing the click event.
framework component
const frameworkComponents = useMemo(
() => ({
customNoRowsOverlay: ({
message,
iconName,
className,
}: ISystemNotification) => (
<SystemNotification
message={message}
iconName={iconName}
className={className}
/>
),
deleteButtonRenderer: (props: IDeleteRowButton) => {
const {
node: {
data: { id },
},
} = props;
return (
<DeleteRowButton
{...props}
setIsRowDeleting={setIsRowDeleting}
isDisabled={typeof id !== 'string'}
ref={deleteRef}
/>
);
},
}),
[ setIsRowDeleting]
);
deleterow component
const DeleteRowButton = forwardRef(
(
{ setIsRowDeleting, node, isDisabled = false }: IDeleteRowButton,
ref?: Ref<IUseRef | MutableRefObject<HTMLElement | null>>
) => {
useImperativeHandle(ref, () => ({ handleClickDeleteRow }));
const isRowDeleting = pathOr(false, ['data', 'isDeleting'], node);
const [isDeleting, setIsDeleting] = useState<boolean>(isRowDeleting);
const { t } = useTranslation();
const isGridDataChanged = useSelector(selectIsGridDataChanged);
const onClick = useCallback(
({ node, setIsDeleting, isRowDeleting, setIsRowDeleting, data }) => {
if (!isNil(data)) {
node.data = data;
}
const rowData = node.data;
setIsDeleting(!isRowDeleting);
let updatedData = {};
if (rowData.isDeleting) {
updatedData = { ...rowData, isDeleting: false };
} else {
updatedData = { ...rowData, isDeleting: true };
}
node.setData(updatedData);
setIsRowDeleting(true);
},
[]
);
const handleDeleteRow = useMemo(
() =>
debounce(() => {
onClick({
node,
setIsDeleting,
isRowDeleting,
setIsRowDeleting,
data: null,
});
}, 0),
[isRowDeleting, node, onClick, setIsRowDeleting]
);
const handleClickDeleteRow = useCallback(
(rowData: RowNode) => {
onClick({
node,
setIsDeleting,
isRowDeleting,
setIsRowDeleting,
data: rowData,
});
},
[isRowDeleting, node, onClick, setIsRowDeleting]
);
// Setting row active if changes was canceled
useEffect(() => {
if (!isGridDataChanged && !isRowDeleting && isDeleting) {
setIsDeleting(false);
}
}, [isDeleting, isGridDataChanged, isRowDeleting]);
return (
<Icon
disabled={isDisabled}
onClick={handleDeleteRow}
data-testid={TestIds.DeleteButtonIcon}
icon='trash'
title={t('Delete')}
/>
);
}
);
here it's working fine when the user is clicking via mouse, I'm trying to integrate keyboard action. but it always updates the last row of the ag-grid. I don't know the reason.
can any one help me in this.

I'm having some trouble trying to change specific button's value when clicked and get back to the previous value with setTimeout()

Here's my example code (I'm trying in a separated component with different Data.
import React, { useState } from 'react';
const initialState = [
{ id: 0, text: 'add to cart' },
{ id: 1, text: 'add to cart' },
{ id: 2, text: 'add to cart' },
];
const Test: React.FC = () => {
const [text, setText] = useState(initialState);
const handleClick = (index: number) => {
const newText = [...text];
newText[index].text = 'Added to cart...';
setText(newText);
setTimeout(() => {
setText(initialState);
}, 2000);
};
return (
<div>
{text.map((buttons, index) => (
<button key={index} onClick={() => handleClick(index)}>
{buttons.text}
</button>
))}
</div>
);
};
export default Test;
You should deep clone your array with JSON.parse(JSON.stringify(...)) since you are editing nested properties.
This code should work:
const handleClick = (index: number) => {
const newText = JSON.parse(JSON.stringify(text));
newText[index].text = 'Added to cart...';
setText(newText);
setTimeout(() => {
setText(initialState);
}, 2000);
};

How do I disable a button in react hooks that has a setState function already inside the onClick?

I am trying to disable a like button that already has an useState for incrementing the likes. And I wanted to disable the button once its clicked.
I would appreciate some help. Thank You!
const allItems = [
{
id: 1,
name: "The Rise and Decline of Patriarchal Systems",
image: "https://img.thriftbooks.com/api/images/i/m/8ECA8C9BAF351D13622ADFFBFA8A5D4E2BAABAFF.jpg",
likes: 3359,
price: 1
}
]
const Shop = () => {
const [items, setItems] = React.useState({allItems, disable: false})
const updateLike = (item) => setItems(items.map(indItem => {
if (indItem !== item) {
return indItem
}
else {
return {...item, likes: item.likes + 1}
}
}))
const listItemsToBuy = () => items.map((item) => (
<div key={item.id} className="card"></div>
<div className="font-text"><h2>{`${item.name}`}</h2>
</div>
<h2>Likes: {item.likes}</h2>
<div>
<button items={items} onClick={()=> updateLike(item, true)}> Like </button>
));```
Inside updateLike function update your state
setItems(prevState => { ...prevState, disable: true, })
Then your button will be look like
<button disabled={items.disabled} >
But preferable to have separate state for this purpose

How to test onClick() funct and useState hooks using jest and enzyme

I am new to this jest+enzyme testing and I am stuck at how to cover the lines and functions such as onClick(), the useState variables and also useffect(). Can anyone with any experience in such scenerios please give me some direction on how to do that efficiently.
Below is the code:
export interface TMProps {
onClick: (bool) => void;
className?: string;
style?: object;
}
export const TM: React.FC<TMProps> = (props) => {
const {onClick} = props;
const [isMenuOpen, toggleMenu] = useState(false);
const handleUserKeyPress = (event) => {
const e = event;
if (
menuRef &&
!(
(e.target.id && e.target.id.includes("tmp")) ||
(e.target.className &&
(e.target.className.includes("tmp-op") ||
e.target.className.includes("tmp-option-wrapper")))
)
) {
toggleMenu(false);
}
};
useEffect(() => {
window.addEventListener("mousedown", handleUserKeyPress);
return () => {
window.removeEventListener("mousedown", handleUserKeyPress);
};
});
return (
<React.Fragment className="tmp">
<Button
className={props.className}
style={props.style}
id={"lifestyle"}
onClick={() => toggleMenu((state) => !state)}>
Homes International
<FontAwesomeIcon iconClassName="fa-caret-down" />{" "}
</Button>
<Popover
style={{zIndex: 1200}}
id={`template-popover`}
isOpen={isMenuOpen}
target={"template"}
toggle={() => toggleMenu((state) => !state)}
placement="bottom-start"
className={"homes-international"}>
<PopoverButton
className={
"template-option-wrapper homes-international"
}
textProps={{className: "template-option"}}
onClick={() => {
onClick(true);
toggleMenu(false);
}}>
Generic Template{" "}
</PopoverButton>
/>
}
Here is the test I have written but it isn't covering the onClick(), useEffect() and handleUserKeyPress() function.
describe("Modal Heading", () => {
React.useState = jest.fn().mockReturnValueOnce(true)
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Button)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Popover)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(PopoverButton)).toHaveLength(1);
});
What you're looking for is enzyme's:
const btn = wrapper.find('lifestyle');
btn.simulate('click');
wrapper.update();
Not sure if it'd trigger the window listener, it's possible you'll have to mock it.

Resources