I am trying to run onClick on a React component that is created from a styled-component, but it does not work without a work-around.
Style 1:
const GenericStyledDeleteButton = styled.button`
height: 30px;
font-size: 20px;
text-align: center;
color: green;
border: none;
border-radius: 5px;
&:hover {
color: red;
}
`;
Component:
const DeleteButton = ({className}) => (
<GenericStyledDeleteButton className={className}
Delete
</GenericStyledDeleteButton>
);
Style 3:
const StyledDeleteButton = styled(DeleteButton)`
margin-left: auto;
margin-right: 25px;
`;
I’ve created a generic styled component, then created a React native component with some text, and then added some further styling onto the generic styled component.
I am trying to perform the following:
<StyledDeleteButton onClick={() => DeleteItem(item._id)} />
I have tried both onClick and onClick but they don’t work. However, it works when I add the following:
const DeleteButton = ({ className, onClick }) => (
<GenericStyledDeleteButton className={className} onClick={onClick}>
Delete
</GenericStyledDeleteButton>
);
The onClick work on other styled-components I have, but not this one.
So my question is, how do I call onClick directly on StyledDeleteButton without changing the DeleteButton component?
To use onClick directly in the styled-component without implementing a work-around.
const DeleteButton = ({ className, onClick }) => (
<GenericStyledDeleteButton className={className} onClick={onClick}>
Delete
</GenericStyledDeleteButton>
);
It is not a work-around. This is how it should work. If the DeleteButton does not accept onClick, you can not pass it. Props are passed from parent to children.
StyledDeleteButton passes onClick to DeleteButton. If the DeleteButton does not have onClick, it can not pass it to its children.
This is the natural way. If you do not want to change DeleteButton, you can make a work-around with context provider, but you have to change how GenericStyledDeleteButton receives onClick. It is not that good as adding an onClick prop to it.
Related
I have this Styled component, where I'm trying to pass DATA-attribute which is coming from props to it. (This is the solution we have on Stack Overflow)
export const InlineEditReadViewErrorContainer = styled.div.attrs(props => ({
'data-cy': props.dataCy
}))`
border: 2px solid #de350b;
border-radius: 3px;
`;
This is how I use this styled component in code
<InlineEditReadViewErrorContainer dataCy='blabla'>
{readView}
</InlineEditReadViewErrorContainer>
But this is doesn't change anything
I think that we must use correctly the attributes that are added to a component in React and more if they are needed only to style a component.
We should use as many native properties as possible but without compromising the private data that would be exposed in the HTML that the client displays in broser, therefore:
If you are going to use data-attributes:
Remember how to name these attributes:
The attribute name should not contain any uppercase letters, and must be at least one character long after the prefix "data-"
Note: I would just use the simplest possible, with booleans to give a set of properties as the first answer described, for example:
component.js
<Error data-active={true}>
{readView}
</Error>
component.styles.js
export const Error = styled.div`
&[data-active="true"] {
border: 2px solid #de350b;
border-radius: 3px;
}
`;
If you want to use custom props without them being displayed in the DOM as the second comment has described, using transient props:
For sample:
component.js
<Error $active={true}>
{readView}
</Error>
component.styles.js
export const Error = styled.div`
${({$active}) => $active ? css`
border: 2px solid #de350b;
border-radius: 3px;
`: null}
`;
The prop should already be "passed" in the sense that it will show up on the component for the purposes of using it in Cypress. If you want to use it internally you could also use transient props such as this
const Comp = styled.div`
color: ${props =>
props.$draggable || 'black'};
`;
render(
<Comp $draggable="red" draggable="true">
Drag me!
</Comp>
);
It was much easier. You can pass the data attribute directly where you use the styled component and everything will be fine.
<InlineEditReadViewErrorContainer data-cy='dateInput'>
{textValue}
</InlineEditReadViewErrorContainer>
Maybe it's related to your bundler, since you should be able to pass a data attribute to a styled-component directly. However, if you're extending an existing component, be aware that you need to pass it through props. Those two situations will attach the data attribute to the final HTML:
function Text(props) {
return (
<p data-foo={props['data-foo']} className={props.className}>
{props.children}
</p>
);
}
const StyledText = styled(Text)`
color: blueviolet;
`;
const StyledHeading = styled.h1`
color: gray;
`;
export default function App() {
return (
<div>
<StyledHeading data-bar="foo">Hello StackBlitz!</StyledHeading>
<StyledText data-foo="bar">
Start editing to see some magic happen :)
</StyledText>
</div>
);
}
I apologise in advance if this is a silly question. Although I have managed to get it to work, I would like to get a deeper understanding.
I am building a custom hamburger menu in react which closes whenever you click anywhere outside the unordered list or the hamburger icon itself.
I have seen answers here Detect click outside React component
And I have followed it but I couldn't understand why it wasn't working.
Firstly when it was just the hamburger icon and no click outside the menu to close option, it worked perfectly.
Then when I used the useRef hook to get a reference to the unordered list in order to only close the menu when the list is not clicked, it worked perfectly except for when I clicked the actual hamburger icon.
After a lot of amateur debugging I finally realised what was happening.
First when I opened the menu the state showMenu changed to true,
Then when I clicked the hamburger icon to close,
The parent wrapper element was firing first instead of the hamburger menu which is strange as during the bubbling phase I would expect the inner most element to fire first.
So the parent element would close the menu changing the state, causing the components to re-render. Then when the event would reach the actual icon the handleClick would once again toggle the state to true giving the impression that the hamburger click isn't working.
I managed to fix this by using event.stopPropogation() on the parent element.
But this seems very strange because I would not expect the parent element's click to fire first especially when Im using bubbling phase.
The only thing I can think of is because it is a native dom addeventlistener event it is firing first before the synthetic event.
Below is the code for the Mobile navigation which has the hamburger
The header component renders the normal Nav or the MobileNav based on screen width. I haven't put code for the higher order components to make it easier to go through, but I can provide all the code if needed:
//MobileNav.js
export default function MobileNav() {
const [showMenu, setShowMenu] = useState(false);
const ulRef = useRef();
console.log('State when Mobilenav renders: ', showMenu);
useEffect(() => {
let handleMenuClick = (event) => {
console.log('App Clicked!');
if(ulRef.current && !ulRef.current.contains(event.target)){
setShowMenu(false);
event.stopPropagation();
}
}
document.querySelector('#App').addEventListener('click', handleMenuClick);
return () => {
document.querySelector('#App').removeEventListener('click', handleMenuClick);
}
}, [])
return (
<StyledMobileNav>
<PersonOutlineIcon />
<MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
{
(showMenu) &&
<ul ref={ulRef} style={{
backgroundColor: 'green',
opacity: '0.7',
position: 'absolute',
top: 0,
right: 0,
padding: '4em 1em 1em 1em',
}}
>
<MenuList/>
</ul>
}
</StyledMobileNav>
)
}
//MenuIcon.js
/**
* By putting the normal span instead of the MenuLine component after > worked in order to hover all div's
*/
const MenuWrap = styled.div`
width: 28px;
position: relative;
transform: ${(props) => props.showMenu ? `rotate(-180deg)` : `none` };
transition: transform 0.2s ease;
z-index: 2;
&:hover > div{
background-color: white;
}
`;
const MenuLine = styled.div`
width: 100%;
height: 2px;
position: relative;
transition: transform 0.2s ease;
background-color: ${(props) => props.showMenu ? 'white' : mainBlue};
&:hover {
background-color: white;
}
`;
const TopLine = styled(MenuLine)`
${(props) => {
let style = `margin-bottom: 7px;`;
if(props.showMenu){
style += `top: 9px; transform: rotate(45deg);`;
}
return style;
}}
`;
const MidLine = styled(MenuLine)`
${(props) => {
let style = `margin-bottom: 7px;`;
if(props.showMenu){
style += `opacity: 0;`;
}
return style;
}}
`;
const BottomLine = styled(MenuLine)`
${props => {
if(props.showMenu){
return `bottom: 9px; transform: rotate(-45deg);`;
}
}}
`;
export default function MenuIcon({showMenu, setShowMenu}) {
const handleMenuClick = (event) => {
console.log('Menu Clicked!');
console.log('State before change Icon: ', showMenu);
setShowMenu(!showMenu);
}
return (
<MenuWrap onClick={handleMenuClick} showMenu={showMenu}>
<TopLine onClick={handleMenuClick} showMenu={showMenu}></TopLine>
<MidLine onClick={handleMenuClick} showMenu={showMenu}></MidLine>
<BottomLine onClick={handleMenuClick} showMenu={showMenu}></BottomLine>
</MenuWrap>
)
}
Reading this article https://dev.to/eladtzemach/event-capturing-and-bubbling-in-react-2ffg basically it states that events in react work basically the same way as DOM events
But for some reason event bubbling is not working properly
See screenshots below which show how the state changes:
Can anyone explain why this happens or what is going wrong?
This is a common issue with competing event listeners. It seems you've worked out that the problem is that the click out to close handling and the menu button click to close handling are both triggered at the same time and cancel each other out.
Event listeners should be called in the order in which they are attached according to the DOM3 spec, however older browsers may not implement this spec (see this question: The Order of Multiple Event Listeners). In your case the click out listener (in the <MobileNav> component) is attached first (since you use addEventListener there, while the child uses the React onClick prop).
Rather than relying on the order in which event listeners are added (which can get tricky), you should update your code so that either the triggers do not happen at the same time (which is the approach this answer outlines) or so that the logic within the handlers do not overlap.
Solution:
If you move the ref'd element up a level so that it contains both the menu button and the menu itself you can avoid the overlapping/competing events.
This way the menu button is within the space where clicks are ignored so the outer click listener (the click out listener) won't be triggered when the menu button is clicked, but will be if the user clicks anywhere outside the menu or its button.
For example:
return (
<StyledMobileNav>
<PersonOutlineIcon />
<div ref={menuRef}>
<MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
{ showMenu && (
<ul>
<MenuList/>
</ul>
)}
</div>
</StyledMobileNav>
)
Then use menuRef as the one to check for clicks outside of.
As an additional suggestion, try putting all the menu logic into a single component for better organization, for example:
function Menu() {
const [showMenu, setShowMenu] = React.useState(false);
// click out handling here
return (
<div ref={menuRef}>
<MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
{ showMenu && (
<ul>
<MenuList/>
</ul>
)}
</div>
)
}
React Component
import React, {useState} from 'react';
import './CounterButton.css';
const CounterButton = (props)=>{
const [currentCount, setCurrentCount] = useState(0);
const handleClick = (event)=>{
if(currentCount == 9){
event.target.classList.toggle('bound-hit');
}
setCurrentCount(currentCount+props.incrementVal);
};
return (
<div class="count-container">
<button onClick={handleClick}>+{props.incrementVal}</button>
<p>{currentCount}</p>
</div>
);
};
export default CounterButton;
External stylesheet for this component
.count-container {
display: flex;
padding: 10px;
}
.count-container > button {
width: 50px;
margin: 0 10px;
}
.bound-hit {
color: red;
}
I have a react component and stylesheet for that component. In this case it toggle class bound-hit to the classList of button. I could select button using event.target and but I want to toggle this class to the <p></p> tag inside my div. My question is how can I select that p tag using event. p tag is like a sibling of button. div with class count-container is parent. I can also select parent div by event.target.parent but I want to select p tag and toggle class bound-hit to that.. How can I do that?
I don't think you need a React specific answer here.
In vanilla JS you can use the nextElementSibling method.
const handleClick = (event) => {
const p = event.target.nextElementSibling
}
Or instead you can do it in CSS with the adjacent sibling combinator.
.bound-hit + p {
// apply styles to the <p> that's just after .bound-hit in the DOM
}
However, if you "manually" add a class in a react component (meaning that this class gets added to the DOM without any representation in the state), some virtual DOM reconciliations might end up removing it.
In a lot of cases, this won't be a problem, but if it is, then you should use a state for it. Here's a simplified example of what that would look like:
const [pClass, setPClass] = useState('')
const handleClick = () => {
setPClass('bound-hit')
}
return (
<p className={pClass} />
)
The question shouldn't be "how to select a sibling" but "how to assign CSS class to the P element on [condition]".
If a React component directly has ownership over the (child) elements you can simple change the components state and apply it to the class list of the element using className.
Doing any DOM manipulation/traversing within a component is mainly bad form using React and overcomplicates the solution.
const CounterButton = (props)=>{
const [currentCount, setCurrentCount] = useState(0);
const [currentClass, setCurrentClass] = useState();
const handleClick = (event)=>{
if(currentCount == 9){
setCurrentClass('bound-hit');
}
setCurrentCount(currentCount+props.incrementVal);
};
return (
<div class="count-container">
<button onClick={handleClick}>+{props.incrementVal}</button>
<p className={currentClass}>{currentCount}</p>
</div>
);
};
I'm not sure why i'm getting this error, i'm just trying to make a button that inverts colors on hover, if you have a solution or a better way to do this please let me know
import React, {useState} from 'react'
function Login() {
const [bttnColor, setBttnColor] = useState('white')
const [textColor, setTextColor] = useState('black')
function colorChange1(){
setBttnColor('black')
setTextColor('white')
}
function colorChange2(){
setBttnColor('white')
setTextColor('black')
}
return (
<div className="Login" style={{display:'flex', flexDirection:'column', justifyContent:'center', alignItems:'center'}}>
<h1 style={{display:'flex', marginTop:'400px', marginBottom:'40px'}}>Login</h1>
<input style={{height:'30px', width:'140px', marginBottom:'10px'}} placeholder='Username'/>
<input style={{height:'30px', width:'140px'}} type='password'placeholder='Password'/>
<button style={{height:'30px', width:'140px', marginTop:'10px', background:bttnColor, color:textColor}} onMouseEnter={colorChange1()} onMouseLeave={colorChange2()}>Login</button>:
</div>
)
}
export default Login
When declaring a property, the result of what's inside of the {} is sent to the Component.
This will send the result of colorChange1() to the component, not the function itself
onMouseEnter={colorChange1()}
This is unwanted behavior in your use case, but remember that this is a property just like any other, like style or className.
You need to pass it a function reference instead of the result of the function. You can do that in two different ways:
onMouseEnter={colorChange1}
onMouseEnter={(event) => colorChange1(event, otherVariables...)}
The first way is a function reference to the existing function. Use this when you don't need to pass any other information to the function
The second way is to wrap the function call with a lambda. This will allow you to take variables from your current scope and pass them into the method when it's run.
EDIT:
On second thought, doing this at all is making it far more complicated than it needs to be. This can be done in a few lines of CSS.
Let's remove those color change methods and the onMouseEnter and onMouseLeave calls, and give the button a className so we can refer to it in CSS.
<button className="login-button">Login</button>:
Then let's create the following css file, named Login.css in the same folder as Login.js:
.login-button {
height: 30px;
width: 140px;
marginTop: 10px;
background:white;
color:black;
}
.login-button:hover {
background: black;
color: white;
}
Finally, let's import the css file:
import "./Login.css";
I am calling a component "MyRadioButton" with following props:
<MyRadioButton
label="Radio Group"
theme="custom-red" //this line
error="Field is required "
radioBtns={options}
id="radioBtns"
name="radioBtns"
getValue={this.getValue}
/>
I have created a react component "MyRadioButton" that will accept color name(theme) as props.
export const MyRadioButton = props => {
const {theme} = props;
return (
<div className="my-radio-buttons"> // need to use theme here
<input
onChange={onChange}
type="radio"
/>
</div>
)}
Based on this prop i want to assign the variable in my components scss file, which will take the color code from my custom defined color pallet.
my-radio-button.scss
/* custom color pallet */
$custom-orange: #F060D6;
$custom-red: #BB532E;
$custom-blue: #4C9FEB;
.my-radio-buttons {
.input{
border: 2px solid $custom-red; // i want to assign the color variable based on input prop value to this property
}
}
I have already tried setting variable at css root with javascript and accessing it with variable function var(), it works fine.
But because of some limitations i dont want to use that approach.
also because the color pallet list is huge, i dont want to use separate classes for all of them.
I am looking for some other solution or different approach.
So you can use a combination of custom css variables and your passed theme property. In you css, you would define the basecolor of the border for example:
.my-radio-buttons {
--theme-color: red;
input {
border: 2px solid var(--theme-color);
}
}
This can be updated by your components via componentDidMount or useEffect with the passed theme:
const MyRadioButton = props => {
const { theme } = props;
React.useEffect(() => {
const input = document.querySelector(".my-radio-buttons input");
input.style.setProperty("--theme-color", props.theme);
}, []);
return (
<div className="my-radio-buttons">
<input />
</div>
);
};
Depending on your code style, you can replace the querySelector with a ref.