I am trying to set up a toolbar on slate using React Hooks. Something seems to be going wrong, and I can't figure out what. Here is my code:
const renderMark = (props, editor, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
case "bold":
return <strong {...attributes}>{children}</strong>
case "italic":
return <i {...attributes}>{children}</i>
case "underline":
return <u {...attributes}>{children}</u>
default:
return next()
}
}
const onClickMark = (event, type, editor) => {
event.preventDefault()
editor.toggleMark(type)
}
<>
<Toolbar>
<Button onPointerDown={event => onClickMark(event, "bold")}>
<BoldIcon />
</Button>
<Button onPointerDown={event => onClickMark(event, "italic")}>
<ItalicIcon />
</Button>
<Button onPointerDown={event => onClickMark(event, "underline")}>
<UnderlineIcon />
</Button>
</Toolbar>
<Editor
onChange={handleChange}
onKeyDown={onKeyDown}
renderMark={renderMark}
renderNode={renderNode}
value={value}
/>
</>
Any idea why this doesn't work?
The issue was that I needed to bind the toolbar to the editor. I did that using the useRef hook as follows.
const editorRef = useRef()
const onClickMark = (event, type, editorRef) => {
event.preventDefault()
editorRef.current.toggleMark(type)
}
return (
<>
<Toolbar>
<Button onPointerDown={event => onClickMark(event, "bold", editorRef)}>
<BoldIcon />
</Button>
...
</Toolbar>
<StyledEditor
...
ref={editorRef}
...
/>
</>
Related
component is like:
<Button and <Icon are customized components which wrap up the <button and <icon
const handleToggleShow = useCallback(() => {
setShow(!show);
}, [show]);
const displayUI = (
<div>
<Icon
testId="editIcon"
onClick={handleToggleShow}
className="edit-icon"
>
</div>
);
const editUI = (
<form data-testid="form" onSubmit={handleSubmit}
<InputComponent />
<Button
testId="saveButton"
text="Save"
disabled={...}
size="large"
color="blue"
type="submit"
/>
<Button
testId="cancelButton"
text="Cancel"
disabled={...}
size="large"
color="grey"
onClick={handleClickCancel}
/>
</form>
);
return(
<div>
{show ? editUI}
{!show? displayUI}
</div>
);
Test is like:
test("show render edit ui when click button", () => {
render(<A {...props} />)
const icon = screen.getByTestId("editIcon");
expect(icon).toBeInDocument();
fireEvent.click(element);
const form = screen.getByTestId("form");
//here throws error: unable to find an element by [data-testid="form"]
expect(form).toBeInDocument();
});
Then I tried queryByTestId("form") and tried getByTestId("saveButton"), it throws error "received value must be an HTMLElement or as an SVGElement",
I was thinking maybe icon click event was not triggered, then I ran this test, still got error
test("show render edit ui when click button", () => {
const handleToggleShow = jest.fn();
render(<A {...props} />)
const icon = screen.getByTestId("editIcon");
expect(icon).toBeInDocument();
fireEvent.click(element);
expect(handleToggleShow).toHaveBeenCalled(); //received number of calls 0
});
Anyone can help? why getByTestId or queryByTestId is not working
Update here:
In the previous test, I didn't pass any mock props to the component.
after passing props, the issue somehow fixed.
I am implementing two text fields next to each other and they have end adornments as a buttons. Those buttons toggle popper visibility. Also each popper has clickawaylistener so the popper is closed when mouse clicks outside popper. If first popper is opened it should be closed when I click button of second text field adornment. Issue is that end adornments have event propagation stopped. I do that to prevent clickaway event when clicking adornment, so to prevent instant closing of popper when it is opened by toggle handler.
I was thinking about wrapping TextField into ClickAwayListener but it didn't work.
P.S. Both TextField will be rendered from separate components and I don't want to share any props between them as they should be independent.
https://codesandbox.io/s/basictextfields-material-demo-forked-rykrx
const [firstPopperVisible, setFirstPopperVisible] = React.useState(false);
const [secondPopperVisible, setSecondPopperVisible] = React.useState(false);
const firstTextFieldRef = React.useRef();
const secondTextFieldRef = React.useRef();
const toggleFirstPopperVisible = (e) => {
e.stopPropagation();
setFirstPopperVisible((prev) => !prev);
};
const handleFirstPopperClickAway = (e) => {
setFirstPopperVisible(false);
};
const toggleSecondPopperVisible = (e) => {
e.stopPropagation();
setSecondPopperVisible((prev) => !prev);
};
const handleSecondPoppertClickAway = (e) => {
setSecondPopperVisible(false);
};
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<div>
<TextField
label="Outlined"
variant="outlined"
inputRef={firstTextFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
edge="end"
onClick={toggleFirstPopperVisible}
>
<Visibility />
</IconButton>
</InputAdornment>
)
}}
/>
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<Popper
open={firstPopperVisible}
anchorEl={firstTextFieldRef.current}
placement="bottom-start"
>
Content
</Popper>
</ClickAwayListener>
</div>
<div>
<TextField
label="Outlined"
variant="outlined"
inputRef={secondTextFieldRef}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
edge="end"
onClick={toggleSecondPopperVisible}
>
<Visibility />
</IconButton>
</InputAdornment>
)
}}
/>
<ClickAwayListener onClickAway={handleSecondPoppertClickAway}>
<Popper
open={secondPopperVisible}
anchorEl={secondTextFieldRef.current}
placement="bottom-start"
>
Content
</Popper>
</ClickAwayListener>
</div>
</div>
);
}
EDIT: Found a temporary solution by wrapping TextField into div and then wrapping tat ClickawayListener. Also prevented propagation on popper itself where needed. This is not ideal, but for my case it worked.
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<div style={{display: inline-box}>
<TextField>
.....
</TextField>
</div>
</ClickawayListener>
<Popper>
....
</Popper>
UPDATED
Wrap the ClickAwayListener in a conditional statement:
{firstPopperVisible && (
<ClickAwayListener onClickAway={handleFirstPopperClickAway}>
<Popper open={firstPopperVisible} anchorEl={firstTextFieldRef.current} placement="bottom-start">
Content
</Popper>
</ClickAwayListener>
)}
...
{secondPopperVisible && (
<ClickAwayListener onClickAway={handleSecondPoppertClickAway}>
<Popper open={secondPopperVisible} anchorEl={secondTextFieldRef.current} placement="bottom-start">
Content
</Popper>
</ClickAwayListener>
)}
Codesandbox Demo
PREVIOUS
I recommend you look at Portals for this. Instead of having multiple elements in the dom, you have one that gets added where needed, as needed.
Portals provide a first-class way to render children into a DOM node
that exists outside the DOM hierarchy of the parent component.
Your single Portal component:
import ReactDOM from 'react-dom';
import React, { useEffect, useState } from 'react';
const Component = ({ content, handleCloseClick }) => {
return <div onClick={handleCloseClick}>{content}</div>;
};
interface PortalProps {
isShowing: boolean;
content: any;
location: any;
handleCloseClick: () => void;
}
const Portal = ({ isShowing, content, handleCloseClick, location }: PortalProps) => (isShowing ? ReactDOM.createPortal(<Component handleCloseClick={handleCloseClick} content={content} />, location.current) : null);
export default Portal;
Which is then used once in your main component:
import React, { useState, useRef } from 'react';
import Widget from './Widget';
import './styles.css';
export default function App() {
const [isShowing, setIsShowing] = useState<boolean>(false);
const [content, setContent] = useState<string>();
const buttonRef = useRef(null);
const handleClick = (e) => {
const { target } = e;
buttonRef.current = e.target.parentNode;
setContent(target.dataset.content);
setIsShowing(true);
};
const handleCloseClick = () => {
buttonRef.current = null;
setContent('');
setIsShowing(false);
};
return (
<div className="App">
<div>
<button onClick={handleClick} data-content={`content for one`}>
One
</button>
</div>
<div>
<button onClick={handleClick} data-content={`content for two`}>
Two
</button>
</div>
<Widget isShowing={isShowing} location={buttonRef} content={content} handleCloseClick={handleCloseClick} />
</div>
);
}
Codesandbox Demo
This question already has answers here:
React Conditional Rendering
(4 answers)
Closed 1 year ago.
This is one of the component
const [mic, setMic] = useState(false)
const micClick = () => {
setMic(true)
alert('micClicked',mic)
}
<VideoControls
micClick={micClick}
/>
this is other component
const VideoControls = ({
micClick,
}) => {
return (
<Card style={style} className={classes.root}>
<Button>
<MicIcon style={iconStyle} onClick={micClick} />
</Button>
<Button>
<MicOffIcon style={iconStyle} onClick={micClick} />
</Button>
}
I want to show the MicIcon as a default whenever button is clicked i want to change it as MicOffIcon , but here i dont have any idea how to utilize it. please help me thanks in advance
Since you have a mic state you can pass it down and then based on that state render whatever icon you feel like, as example:
const [mic, setMic] = useState(false)
const micClick = () => {
setMic(true)
alert('micClicked',mic)
}
<VideoControls
micClick={micClick}
mic={mic}
/>
and then inside your VideoControls component:
const VideoControls = ({
micClick,
mic
}) => {
return (
<Card style={style} className={classes.root}>
<Button>
{mic ?
<MicIcon style={iconStyle} onClick={micClick} /> :
<YourOtherIcon/> }
</Button>
<Button>
<MicOffIcon style={iconStyle} onClick={micClick} />
</Button>
}
<VideoControls
micClick={micClick}
mic={mic}
/>
Pass mic as a prop to your VideoControls component. And set your icons according to the value of mic.
const [mic, setMic] = useState(false)
const micClick = () => {
setMic(mic => !mic); // change the value of mic
alert('micClicked',!mic)
}
<VideoControls
micClick={micClick}
mic={mic}
/>
VideoControls Component
const VideoControls = ({
micClick, mic
}) => {
return (
<Card style={style} className={classes.root}>
{mic ?
<Button>
<MicIcon style={iconStyle} onClick={micClick} />
</Button>
:
<Button>
<MicOffIcon style={iconStyle} onClick={micClick} />
</Button>
}
}
You could pass your mic state variable to the VideoControls component and display the relevant Icon based on its value:
<VideoControls
micClick={micClick}
mic={mic}
/>
and
const VideoControls = ({
micClick,
mic
}) => {
return (
<Card style={style} className={classes.root}>
<Button>
{mic ?
<MicIcon style={iconStyle} onClick={micClick} /> :
<MicOffIcon style={iconStyle} onClick={micClick} />
}
</Button>
</Card>
);
I have a button inside a cart and I want give it an animation whenever the cart is opened. To target it I used useRef and tried to log in the console the result:
const checkoutBtnRef = useRef("null");
useEffect(() => {
console.log(checkoutBtnRef.current);
}, [cartOpen]);
The problem is that current is null when I open the cart and returns the actual button only when I close the cart, I expected it to be the opposite.
Any idea?
FULL COMPONENT CODE
export default function TopNav() {
const classes = useStyles();
const [cartOpen, setCartOpen] = useState(false);
const checkoutBtnRef = useRef("null");
useEffect(() => {
console.log(checkoutBtnRef.current);
}, [cartOpen]);
return (
<>
<Container maxWidth="xl">
<div className={classes.root}>
<CardMedia
image="https://www.example.com"
className={classes.media}
/>
<SearchBar placeholder="Search" className={classes.searchBar} />
<div className={classes.iconsContainer}>
<PersonIcon className={classes.icon} />
<FavoriteBorderIcon className={classes.icon} />
<Badge
invisible={false}
variant="dot"
color="error"
onClick={() => setCartOpen(true)}
>
<ShoppingBasketIcon className={classes.icon} />
</Badge>
</div>
</div>
</Container>
<Drawer
classes={{
paper: classes.cart,
}}
anchor="right"
open={cartOpen}
transitionDuration={{ enter: 500, exit: 200 }}
onClose={() => setCartOpen(false)}
>
<div className={classes.topCartContent}>
<Typography variant="h5">Cart</Typography>
<CloseIcon
className={classes.closeIcon}
onClick={() => setCartOpen(false)}
/>
</div>
<Divider />
<List>
<CartItem />
</List>
<Button
classes={{
root: classes.checkoutBtn,
}}
variant="outlined"
onClick={() => setCartOpen(false)}
ref={checkoutBtnRef}
>
checkout
</Button>
</Drawer>
</>
);
}
EDIT :
your drawer has delay and your button didn't render yet so u cant see button on cartOpen change event and ...
so use this for storing and call your functions instead of useEffect.
const checkoutBtnRef = (ref) => {
console.log(ref);
if (ref){
// do ur staff here
}
};
If u need useEffect u can do it like this :
const [btnRef, setBtnRef] = useState(null);
const checkoutBtnRef = (ref) => {
if (ref) setBtnRef(ref);
};
useEffect(() => {
console.log(btnRef);
}, [btnRef]);
Old answer:
const App = ()=>{
const [cart,setCart]= React.useState(false);
const [btnRef, setBtnRef] = React.useState(null);
const checkoutBtnRef = (ref) => {
if (ref) setBtnRef(ref);
else setBtnRef(null)
};
React.useEffect(() => {
console.log(btnRef);
}, [btnRef]);
const cartHandler = () => {
setTimeout(()=>{
setCart((prev) => !prev)
},1000);
};
return (
<React.Fragment>
<button onClick={cartHandler}>
Cart
</button>
{cart && <button ref={checkoutBtnRef}>Check Out</button>}
</React.Fragment>
)
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I am new in react, i am trying to get component based on if button is clicked and once the component is returned to Ui, i want to change button state back to original(false),so if i click on that button again i should get again new component below to the previous one,and this should just go on..,
in below code i am able to get this just one time.
const Command = () => {
const [buttonState, setButtonState] = useState(false);
const AndOrHandler = () => {
if (buttonState) {
return (
<div>
<Grid container>
<SomeComponent />
</Grid>
</div>
);
}
return null;
};
return (
<Fragment>
<FormControl>
<SomeComponent />
<AndOrHandler />
<Button onClick={() => setButtonState(true)} variant="contained" color="primary">Add Condition</Button>
</FormControl>
</Fragment>
);
};
export default Command;
You want continue adding component, i suggest have state components:
const Command = () => {
const component = <div>
<Grid container>
<SomeComponent />
</Grid>
</div>;
const [components, setComponents] = useState([]);
return (
<Fragment>
<FormControl>
<SomeComponent />
{components.map(component => component)}
<Button onClick={() => setComponents([...components, component])} variant="contained" color="primary">Add Condition</Button>
</FormControl>
</Fragment>
);
};
export default Command;