i want to improve my code, with several buttons that has custom class names (attr), when clicked should add to body tag (toggle), now is adding the first button only because for ("button")[0] but should work for each button
import React, { useState, useEffect } from "react"
function Test() {
const [isClass, setIsClass] = useState(false)
useEffect(() => {
const x = document.getElementsByTagName("button")[0].getAttribute("custom-class")
document.body.classList.toggle(x, isClass)
}, [isClass])
return (
<>
<button custom-class='test1' onClick={() => setIsClass(!isClass)}>
Setting test1 className
</button>
<button custom-class='test2' onClick={() => setIsClass(!isClass)}>
Setting test2 className
</button>
</>
)
}
export default Test
Thanks
Please use this code.
let oldStyle = "";
const handleClick = (index) => {
const x = [...document.getElementsByTagName("button")].map(value => value.getAttribute("custom-class"));
document.body.classList.contains(x[index]) ? document.body.classList.remove(x[index]) : document.body.classList.add(x[index]);
if(document.body.classList.length > 1) document.body.classList.replace(oldStyle, x[index]);
oldStyle = x[index];
}
return (
<>
<button custom-class='test1' onClick={() => handleClick(0)}>
Setting test1 className
</button>
<button custom-class='test2' onClick={() => handleClick(1)}>
Setting test2 className
</button>
</>
)
It is better not to use DOM querying and manipulation directly with elements that are created and controlled by react. In your particular example it is ok to use document.body, but not ok to search for buttons, especially when you try to find them by tag name. To actually toggle a class in classList you don't need second parameter in most cases, so additional state is also not needed.
React way to get reference to element renderend by React would be to use Ref. However, in your particular case side effect can be launched inside event handler, so you don't need useEffect or useRef.
Your onClick handler can accept event object that is Synthetic Event. It holds property target that holds reference to your button.
So, the easiest way would be simply to write like this:
function Test() {
function clickHandler(event) {
let classToToggle = event.target.getAttribute("custom-class");
document.body.classList.toggle(classToToggle);
}
return (
<>
<button key="test1" custom-class="test1" onClick={clickHandler}>
Setting test1 className
</button>
<button key="test2" custom-class="test2" onClick={clickHandler}>
Setting test2 className
</button>
</>
);
}
export default Test;
If you need to have only single className from the list, you can decide which class to enable or disable with a bit of a state. Since anything can add classes on body it might be useful to operate only on some set of classes and not remove everything.
Also, not mentioned before, but consider using data attribute as its purpose is to keep some additional data.
function Test() {
// this can come from props or be hardcoded depending on your requirements
// If you intend to change it in runtime, consider adding side effect to cleanup previous classes on body
let [classesList] = React.useState(["test1", "test2"]);
let [activeClass, setActiveClass] = React.useState("");
// You can switch actual classes in effect, if you want to
function clickHandler(event) {
let classToToggle = event.target.dataset.customClass;
// we remove all classes from body that are in our list
document.body.classList.remove(...classesList);
if (activeClass === classToToggle) {
setActiveClass("");
} else {
// if class not active - set new one
document.body.classList.add(classToToggle);
setActiveClass(classToToggle);
}
}
return (
<>
{classesList.map((cn) => (
<button key="cn" data-custom-class={cn} onClick={clickHandler}>
Setting {cn} className
</button>
))}
</>
);
}
Related
I have a button named yes in the child component where I delete a list item from array and local storage using the props.id passed from the parent component.
The problem is the item is deleted from array and local storage but is still visible on the screen until I press delete button on another item in the parent component.
when I press delete button in the parent component it opens an overlay. When I press yes button on overlay I want list item to be removed from the screen immediately.
here is the code in the child component.
import React, { useCallback, useState } from "react";
import styles from "./Delete.module.css";
function Delete(props) {
// console.log();
const store = props.store;
const [no, setNo] = useState(false);
let [deleted, setDelted] = useState(store);
console.log(deleted);
console.log("Length :" + store.length);
const noBtnHandler = () => {
console.log("clicked");
setNo(!no);
props.setDel(false);
};
const yesBtnHandler = () => {
console.log("Dlete.js :" + props.id);
const filteredStore = deleted.filter((task) => task.id !== props.id);
setDelted([...filteredStore]);
localStorage.removeItem(props.id);
// console.log(deleted);
setNo(!no);
};
return (
<div className={`${no === false ? styles.del : styles.delOn}`}>
<section>
<div>
<h3>Are you Sure ?</h3>
</div>
<div>
<button type="button" onClick={yesBtnHandler}>
{" "}
Yes{" "}
</button>
<button type="button" onClick={noBtnHandler}>
{" "}
No{" "}
</button>
</div>
</section>
</div>
);
}
export default Delete;
You are passing store from the parent component to the Delete Component and setting a new state here 'deleted'. so you are only calling the setDeleted on the Delete component which wont affect the parent component.
The correct implementation is to have the store state in the parent component if you don't already have it. It is will still be same like deleted state but possibly with a better name. Say const [store, setStore] = useState([])
Define a function to filter out a particular record just like you have in the yesBtnHandler handler. but this function will be defined in the parent component. Say as an example
const removeRecord = (id) => {
const filteredStore = store.filter((task) => task.id !== id);
localStorage.removeItem(id);
setStore(filteredStore);
}
You now need to pass the a function to the Delete Component from the parent rather than passing the whole store. Like
<Delete removeFunc= {() => { removeRecord(id) }} />
After passing this function, you need to call it in your yesBtnHandler function. Like
function Delete({removeFunc}) {
...
const yesBtnHandler = () => {
removeFunc();
setNo(!no);
};
}
Try remove the trailing ...
const yesBtnHandler = () => {
console.log("Dlete.js :" + props.id);
const filteredStore = deleted.filter((task) => task.id !== props.id);
setDelted([filteredStore]);
//or setDelted(filteredStore);
localStorage.removeItem(props.id);
// console.log(deleted);
setNo(!no);
};
my understanding of this is that you're trying to change the state of a parent component from a child component. If that's what you're intending to do then you can do the following:
Define the function Delete(id) {...} inside the parent component rather than the child component.
Next, you'll have to pass both the function and the array to your child component, something like this: <ChildComponent array={array} onDelete={Delete}, where array is the array in your parent component and Delete is the function to delete an item from the array.
Finally, in your child component, with the props passed in correctly, i.e, function ChildComponent({array, Delete}) {...}you can now have access to the array in your parent component, and actually modify it like you'd like. To fire the event listener on the yes button in the child component, do this: <button type="button" onClick={() => Delete(array.id)}> {" "} Yes{" "} </button>
I hope this will be helpful
I have a multi page form that renders a list of buttons for each possible answer.
I am currently using getElementByID to change the button style when a button is clicked. I think this is not considered good practice in react.
can I use the useRef hook to target the DOM element and change its style instead of using getElementByID if the buttons are dynamically displayed with map?
{answers.map((answer, count = 0) => {
return (
<Button
key={count}
id=`button${count}`
onClick={(e) => {
document.getElementById(`${e.currentTarget.id}`).style.color = "white";
}}
>
<ButtonTitle>{answer.name}</ButtonTitle>
</Button>
);
})}
(animations on safari are breaking the active style, so I can't use the active pseudo element)
You can do this by adding a new state and toggle active class by that state.
Code something like this.
const [activeindex, setActiveIndex] = useState("");
return(
<>
{answers.map((answer, count = 0) => {
return (
<Button
key={count}
className={count==activeindex?"acvie":""}
onClick={(e) => {
setActiveIndex(count)
}}
>
<ButtonTitle>{answer.name}</ButtonTitle>
</Button>
);
})}
}
</>
And use .active class in css file.
Imagine I have a page "Parent" which conditionally renders a div "Child".
On the click of a button, "Child" opens. To close "Child" one has to click in a X button inside it.
This is how I would do it and in my opinion it looks clean.
const Parent = (props) => {
const [childVisible, setChildVisible] = useState(false);
return (
<>
{childVisible && <Child close={setChildVisible.bind(false)} />}
<button onClick={setChildVisible.bind(true)}>
Open Child
</button>
</>
)
}
const Child = (props) => {
return (
<div>
<p>Im Child</p>
<button onClick={props.close()}> X </button>
</div>
)
}
Since react v16.13.0 react has introduced a warning Warning: Cannot update a component from inside the function body of a different component. and it seems I can't do this anymore.
What's the correct pattern now? I would rather not have a state in both components stating the same thing.
Call back was not properly added .You could do like this onClick={props.close}
While use onClick={props.close()} like this. close() function run on child mount instead of click event
const Child = (props) => {
return (
<div>
<p>Im Child</p>
<button onClick={props.close}> X </button>
</div>
)
}
I have started an application which I want to work same as weather.com next 36 hours section. The idea is when you click on each weatherCard which has a seperate component in my app you will update the below section which is my weatherDetails component based on the selected weatherCard /weather box. So I made the entire component clickable by giving it the click event via props from my stateful component which is my weatherLocation component. This is my WeatherCard component:
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={props.clicked}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
And here in render method in WeatherLocation component I loop through data coming from state and give props the WeatherCard component:
const WeatherCards = this.state.reports.map( report => {
return(
<WeatherCard
key={report.id}
{...report}
clicked={() => this.handleCardClick(event)}
/>
);
});
And this is the handleCardClick that I added for it just for testing:
handleCardClick = event => {
// const { reports , selectedCardInfo , activeCard } = this.state;
const selectedDate = document.getElementById(event.target.id);
console.log(event.target.id);
}
I don't want to use anchor tag as I don't need href. The click works fine by itself. But because I need to get the id of the parent which is the div with the class of weatherCard. At the moment when I click on other elements inside the card I cannot get the id because they are not the parent. The reason I need its id is when I get data with from the API I need a unique value for each card so that when you click on the card the data for that card will be shown in the other component which is the WeatherDetails component. But for now I need to be able to somehow choose that selected card and pull out the state for that unique card. Could someone help me out? Thanks.
You just need to pass the Parent component ID to your onClick function in Weather Card.
Here is your WeatherCard - Component
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={event => props.clicked(event, props.id)}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
You can see that I have added props.id to your onClick function and with help of event now you can access that id from the parent component.
Now here is your Parent Component- WeatherCards
const WeatherCards = this.state.reports.map( (report, i) => {
return(
<WeatherCard
key={report.id}
id={i}
{...report}
clicked={this.handleCardClick}
/>
);
});
You can see in the code I am passing index number as id to your child component.
So this will give you an id (for now it's an index number) of the card in your onClick handler.
and Finally, here is your on click handler.
handleCardClick = (event, weatherCardID) => {
console.log(weatherCardID)
}
As of now, I am using the index as id if you want to use a unique identifier, you can change that easily.
General JavaScript solution is to differentiate the elements and .stopPropogation after you've captured the event you are targeting. A nested unordered list, <ul>would be an example. Tag the containing <li> with an .opened class upon rendering/displaying each level of nesting, tag those <li> elements accordingly, e.g. a dataset attribute such as data-make, then data-model, then data-option. You then attach and fire event listeners on the different level <li>'s.
Thank you #RutulPatel. I made your answer as the answer. But I changed your code a bit as I got your point so I wrote an answer as it is long. I think we might not need to change the WeatherCard at all and I don't pass event or any logic there. so it will be intact:
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={event => props.clicked(event, props.id)}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
But I use your tip changing my weatherCards array to look like this:
const weatherCards = this.state.reports.map( report => {
return(
<WeatherCard
key={report.id}
id={report.date}
{...report}
clicked={() => this.handleCardClick(event, report.date)}
/>
);
});
So I use the report.date which is a unique value as my id. Also I don't pass event as a parameter to the arrow function I just pass it with the report.date to the handler:
clicked={() => this.handleCardClick(event, report.date)}
And the handler will be the same as you did:
handleCardClick = (event, weatherCardID) => {
console.log(weatherCardID)
}
I might even remove event later on from both if there was no need fo that.
Thank you again.
In react I have a code like this:
var myButtons=[];
/*Products is an array of objects where each object identify a product*/
for (var p of Products) {
var button = <button
style={someStyle}
onClick={onClickFunction}>
p.name
</button>
myButtons.push(button)
}
I will use this react array of buttons on a render command. The problem I have is that I do not know how to make one of these buttons to show its label p.name through the onClickFunction.
A simpler more user friendly way is to iterate the data with a function. (note that this does not take into account scope, so this may be needed if it's inside a component)
function makeButton(data) {
return (
<button
style={someStyle}
onClick={() => onClickFunction(data.label)}> //pass parameter for callback here if binding isn't used
data.name
</button>
);
}
Now you can simply use a binding map inside your div!
<div>
{Products.map(makeButton, this)}
</div>
You can add your label as paremeter :
<button style={someStyle} onClick={p.name => onClickFunction(p.name)}>
p.name
</button>
And :
onClickFunction = (label) => () =>{
console.log(label)
}
The easiest way is to use ES6 syntax and array map.
The name property should be unique, and don't forget provide a key for each button:
const myButtons = Products.map(p => (
<button style={someStyle} onClick={e=>{ this.onClickFunction(e, p.name); }} key={p.name}/>
{p.name}
</button>
));
Using an arrow function, so it doesn't require .bind(this). Add e.preventDefault() to prevent default behavior, if the buttons are in a form.
onClickFunction = (e, name) => {
e.preventDefault();
// Your button behavior goes here.
}