How to attach close function on dyanmic children button? - javascript

I have a Tooltip component which is a wrapper for dynamic content.
I am trying to use it as a popup, which will have Delete and Cancel buttons.
I am passing the delete and cancel button as children prop, the problem is cus Close and Open state is in the Tooltip component.
I need to attach the close function on Cancel button which lives in Children prop.
Need help to find an elegant solution to this.
Tooltip component:
export const Tooltip: FC<TooltipProps> = ({
content,
helperText,
children,
...props
}) => {
const [visible, setVisible] = useState(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
return (
<div>
<Tippy
content={content}
visible={visible}
onClickOutside={hide}
interactive
{...props}
>
<div onClick={visible ? hide : show}>
// =====>>> **Close button which be in children prop, need to attach hide() function**
{children}
</div>
</Tippy>
</div>
);
};
This is Call of Tooltip component and passing buttons as Children:
<Tooltip
content={
<div className="popover-buttons">
// Need to attach here **hide()** function from Tooltip coomponent
<button>
Cancel
</button>
<button>
Delete
</button>
</div>
</Tooltip>

You can make the content prop passed to Tippy a component that has hide() function passed to it as a prop
export const Tooltip: FC<TooltipProps> = ({
content: Content,
helperText,
children,
...props
}) => {
const [visible, setVisible] = useState(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
return (
<div>
<Tippy
content={<Content hide={hide} />}
visible={visible}
onClickOutside={hide}
interactive
{...props}
>
<div onClick={visible ? hide : show}>
// =====>>> **Close button which be in children prop, need to attach hide() function**
{children}
</div>
</Tippy>
</div>
);
};
Then you have:
<Tooltip
content={ ({ hide }) =>
<div className="popover-buttons">
// Need to attach here **hide()** function from Tooltip coomponent
<button onClick={hide}>
Cancel
</button>
<button>
Delete
</button>
</div>
</Tooltip>

Related

React setting the state of all rendered items with .map in parent component

I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}

Propagate action between react components

So, I have re-used a login/register modal and added a Modal to be able to show/hide it.
So I have implementend in the index.js below:
const Form = ({ initialState = STATE_SIGN_UP, showPopUp = STATE_HIDE }) => {
const [mode, toggleMode] = useToggle(initialState);
const [display, toggleDisplay] = useToggleDisplay(showPopUp);
return (
<Modal className="modal" show={showPopUp} size="lg">
<Container pose={mode === STATE_LOG_IN ? "signup" : "login"}>
<div className="container__form container__form--one">
<FormLogin mode={mode} />
</div>
<div className="container__form container__form--two">
<FormSignup mode={mode} />
</div>
<Overlay toggleMode={toggleMode} mode={mode} />
</Container>
</Modal>
);
};
and I have added in FormLogin a close icon and added onClick. I used a console.log to verify if it's working. However, I do not understand how to be able to send and make the modal in the index.js closed when the action on close happened in the FormLogin class.
I tried to use the toggleDisplay but I have lost my track doing it. toogleDisplay is done as below:
export const STATE_SHOW = true
export const STATE_HIDE = false
const useToggleDisplay = initialState => {
const [display, setDisplay] = useState(initialState)
const toggleDisplay = () =>
setDisplay(display === STATE_SHOW ? STATE_HIDE : STATE_SHOW)
return [display, toggleDisplay]
}
export default useToggleDisplay
Any idea ?
Just pass an onClose handler(which equals toggleMode here) to your modal and call it every time the close button is clicked.
You need to call toggleDisplay in order to close your modal, pass it into your form component
<FormLogin mode={mode} toggleDisplay={toggleDisplay} />
Then inside FormLogin component wherever your click handler is for closing the modal pass in toggleDisplay(false)
<button onClick={() => toggleDisplay(false)}>Close</button>

Props not being passed down to Material UI button text

I have a scenario in my app where I use data attributes on buttons to capture certain contextual information in the modal that is launched by pressing the button. This relies on the data attributes being available on e.target.dataset. The problem is if I click on the part of the button with text, the target is a that appears to be generated by Material-UI but which doesn't contain the props I passed down to the button.
Here's my button implementation (Button.js) [SmallButton and LargeButton are styled versions of the Material Button]:
const Button = ({ buttonText, buttonSize, handleClick, pink, ...props }) => {
return (
<>
{buttonSize === 'large' ? (
<LargeButton
variant="contained"
fullWidth={true}
size="large"
onClick={handleClick}
pink={pink}
{...props}
>
{buttonText}
</LargeButton>
) : (
<SmallButton
variant="contained"
size="small"
onClick={handleClick}
pink={pink}
{...props}
>
{buttonText}
</SmallButton>
)}
</>
);
};
This is where the button is used:
<AddContainer flexDirection="column">
<Label {...props}>Add {type}</Label>
<Button
buttonSize="small"
buttonText="+ Add"
pink="true"
data-show-only={lowerCase(type)} // this needs to come through in e.target.dataset when button clicked
onClick={launchModal}
/>
</AddContainer>
<ModalContainer
open={open}
context={context}
handleClose={closeModal}
initialScreen="Add Exercise"
addWorkout={addWorkout}
/>
The context object above should include a property called showOnly. Here is the custom hook where we create the context object.
const useModal = (initialValues = {}) => {
const [open, setOpen] = useState(initialValues);
const [context, setContext] = useState({});
const launchModal = e => {
const targetContext = { ...e.target.dataset };
setContext(targetContext);
setOpen(true);
};
const closeModal = e => {
setOpen(false);
setContext({});
};
return {
open,
context,
launchModal,
closeModal,
};
};
If I click on the outside of the button, this works as designed, with e.target.dataset containing a showOnly property that is passed on to the context object. If you click on the text, e.target is a and e.target.dataset is empty.
You want to use currentTarget instead of target.
From https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget:
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred and which may be its descendant.
Here's a little example that you can use to see the difference in the console logs:
import React from "react";
import Button from "#material-ui/core/Button";
export default function App() {
return (
<div className="App">
<Button
data-test="1"
onClick={e => {
console.log("target", e.target.dataset);
console.log("currentTarget", e.currentTarget.dataset);
}}
>
Hello
</Button>
</div>
);
}

Next.js show/hide divs on buttons

I am trying to show and hide divs on buttons in Next JS.
This post helped me a lot:
Next.js toggle display of a div tag
I used this solution using useState in my case and it works perfectly. The difference is that I have many buttons and each of them supposes to show different div and hide active div.
This is what I am trying to achieve
https://codepen.io/CthuKi/pen/YqZvRv
export default function Header(){
const [showMe, setShowMe] = useState(false);
function toggle(){
setShowMe(!showMe);
}
return (
<>
<button onClick={toggle}>Button 1</button>
<button onClick={toggle}>Button 2</button>
<button onClick={toggle}>Button 3</button>
<div>
<div style={{display: showMe?"block":"none"}}>
Show this div on button 1 and hide other active
</div>
<div style={{display: showMe?"block":"none"}}>
Show on button 2 and hide other active
</div>
<div style={{display: showMe?"block":"none" }}>
Show on button 3 and hide other active
</div>
</div>
</>
);
}
I will appreciate any help
One solution would be to have multiple toggle functions to toggle multiple divs. But there is a better way, create only one state variable showAll and one function toggleAll to show all the content.
To toggle one button to show specific content, create an array of data that you want to show, and just change the index to show the current data from the array.
For example, see the codesandbox for demo
import React, { useState } from "react";
export default () => {
const [data, setData] = useState(["hello", "hi there", "holla"]);
const [showAll, setShowAll] = useState(false);
const [currentIdx, setCurrentIdx] = useState(0);
const [showCurrent, setShowCurrent] = useState(false);
const toggleAll = () => {
setShowAll(val => !val);
setShowCurrent(false);
};
const toggleCurrent = () => {
if (!showCurrent) {
setShowCurrent(true);
setShowAll(false);
return;
}
};
const setCurrent = index => {
setCurrentIdx(index);
toggleCurrent();
};
return (
<div>
<div>
<button onClick={toggleAll}>{showAll ? "Hide All" : "Show All"}</button>
<button onClick={() => setCurrent(0)}>First</button>
<button onClick={() => setCurrent(1)}>Second</button>
<button onClick={() => setCurrent(2)}>Third</button>
</div>
<div>
{showAll && data.map((el, i) => <p key={`content-${i}`}>{el}</p>)}
</div>
{showCurrent ? <div>{data[currentIdx]}</div> : null}
</div>
);
};

React Hooks: Transition don't work with Node as Prop

I have a Dropdown made with React Hooks. The button should have Arrow, which rotate. My first Version works fine:
const DropdownMenu= (props) => {
const [open, setOpen] = useState(false);
const openDropdown = (): void => setOpen(prevState => !prevState);
return (
<div>
<Button
onClick={openDropdown}
dropdownIsOpen={open}
>
Text </Button>
<DropdownContent isOpen={isOpen} />
</div>
);
};
export default DropdownMenu;
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
export default Button;
But now I have multiple Dropdowns and want to use them with the same component. I gave the toggleNode as prop. Now the Arrow rotate but without the transition:
const DropdownButton = (props) => {
const [isOpen, setOpen] = useState(false);
const onToggle = (): void => setOpen(prevState => !prevState);
return (
<Dropdown
isOpen={props.isOpen}
onToggle={onToggle}
toggleNode={
<Button dropdownIsOpen={isOpen}>
Text
</Button>
}
/>
);
};
export default DropdownButton;
Have someboy an idea?
Thank you!
my guess is, you are exporting different Button
How can you use Button inside Button, possibly by importing Button from somewhere else. And then you are again exporting Button. This will be confuse for transpiler, as which Button to be exported.
You might want to rename your custom Button to something else
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
export default Button;
Given your Button component takes an onClick prop:
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
NOTE: As #SagarMore points out, there my also be a naming collision between some imported Button component and your Button component.
You may just need to pass a callback to Click (terrible name, BTW, should be onClick, hopefully it was just a typo):
const DropdownButton = (props) => {
const [isOpen, setOpen] = useState(false);
const onToggle = (): void => setOpen(prevState => !prevState);
return (
<Dropdown
isOpen={props.isOpen}
onToggle={onToggle}
toggleNode={
<Button onClick={onToggle} dropdownIsOpen={isOpen}>
Text
</Button>
}
/>
);
};
Passing onToggle to the inner button's onClick handler should now toggle the isOpen state of the DropDown.
This was my fault.
In my Dropdown component I render two different states conditional. So it renders the start or the end state and don't use the animation. I have to render a animation and don't render new when I click so the animation works.

Categories