In my parent component, I used a component called List as follows.
render() {
return (
<div className="experiments">
<div className="experiments-list-container">
<List rowItems={this.state.employeeData} />
</div>
</div>
);
}
}
In my List component, I am trying to change the style whenever each item of the row is clicked. So what I did is:
render() {
const dateDisplay = moment(this.props.createdAt).format('MMM YYYY');
return (
<tr
className={this.state.isExpanded ? 'testclass' : "experiment-list__row"}
//className="experiment-list__row"
onClick={this.handleRowClick}
>
<td>
{this.props.rowItems.firstName + ' ' + this.props.rowItems.lastName}
</td>
<td>{this.props.rowItems.jobTitle}</td>
<td>{'Email#Email.com'}</td>
<td>{this.props.rowItems.employmentType}</td>
</tr>
);
}
whenever I click a row in the table, it will all a function that changes the this.state.isExpanded to True. However, the style that I actually want to change is <div className="experiments"> or <div className="experiments-list-container">. But I am not sure how to change the style of the upper-level component. Please help.
EDIT
Thanks for the reply. What I have tried is,
const List = props => {
return (
<table className="experiment-list">
<tbody>
<ListHeader />
{props.rowItems.map((data, i) => <ListRow
key={i}
rowItems={data}
onRowClicked={props.onRowClicked} />)}
</tbody>
</table>
);
};
and
toggleEmployerInfo(e) {
alert('dd')
}
in my parent component.
Whenever I click each row, it alerts "dd" correctly.
However, what I eventually want to do is pass in the info of the row clicked.
In my parent component, I use the List by doing
<div className="experiments-list-container">
<List
rowItems={this.state.employeeData}
onRowClicked={this.toggleEmployerInfo.bind(this)}
/>
</div>
This does render all data into each row correctly, but how can I make each row correctly read the id of the item that the row has?
You could pass an event handler to the List component and call it whenever a row is clicked. Here I've defined handleRowClick in the parent component as an ES6 arrow function. Then I pass this function as a callback to the child component via the onRowClicked prop.
// parent.jsx
handleRowClick = (id) => {
// Handle click event, update state, etc.
console.log(id);
}
render() {
return (
<div className="experiments">
<div className="experiments-list-container">
<List rowItems={this.state.employeeData} onRowClicked={this.handleRowClick} />
</div>
</div>
);
}
And then call the onRowClicked function on the onClick event for each element you want to react to.
// list.jsx
render() {
return (
// Extremely simplified example...
<div onClick={() => this.props.onRowClicked('row-id-goes-here')}>row content</div>
);
}
The arrow function syntax here allows us to specify parameters beside the default event parameter that you would get if you just used onClick={this.props.onRowClicked}.
Here is a post explaining this approach better than I can: https://medium.com/#machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb
Related
I'm trying to make an accordion, that opens up upon clicking the "+" icon. But when I click on a single element, all the other elements expand. Is there a way to just open one of the accordions? I've added the screenshot of the accordion and also added the code.
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
const [display, setDisplay] = useState(false);
const clickHandle = () => {
setDisplay(!display);
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{data.map((question) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
<button className="btnContainer" onClick={clickHandle}>
{display === false ? (
<AiFillPlusCircle size="2.4em" />
) : (
<AiFillMinusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{display === false ? (
<></>
) : (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Explanation
Basically you can't use a single variable to toggle all your elements. All the elements will act as a single element, so either all will open or all will close.
You need something that can be checked against each element. Now id is a potential candidate but it has it's drawbacks, since it's a list the best option is using the index of the element itself.
So you first change the display type from boolean (false) to integer type and default it to -1 (anything less than zero)
Then change your .map function from .map((question) =>... to .map((question, questionIndex) =>..., this will get you a variable questionIndex which holds the current question's index
You can use that (questionIndex) and the display variable to check against each other and display the appropriate states.
Benefits when compared to other answers
Since you are dealing with a list of items, it is always best to use the index of an element to toggle the element's display, This ensures you have decoupled your View from your Data. (As much as possible)
If for some reason your id is null or duplicate, it will create issues in your display.
It is easier to just call toggleElement(2) to automatically open an element for a given position via code (on first load). This is useful if you want to maintain the open states between url changes / reloads, you just add the index to the query parameter of the url.
Solution
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
// Update the display to be of type integer and init it with -1
const [display, setDisplay] = useState(-1);
// Add a parameter to the click function to take the clicked element's index
const toggleElement = (currentIndex) => {
// Check if the element that is clicked is already open
if(currentIndex===display) {
setDisplay(-1); // If it is already open, close it.
}
else {
setDisplay(currentIndex); // else open the clicked element
}
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{/* Add a variable questionIndex to the map method to get the index of the current question */}
{data.map((question, questionIndex) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
{/* Update the button onClick event to pass the current element's index via the questionIndex variable */}
<button className="btnContainer" onClick={()=> toggleElement(questionIndex)}>
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<AiFillMinusCircle size="2.4em" />
) : (
<AiFillPlusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
) : (
<></>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Instead of a boolean, use an id in your state
const [display, setDisplay] = useState("");
When you map an item, add this function
const { id, title, info } = question;
const handleSetDisplay = () => {
if(id === display) {
//Close panel
setDisplay("")
} else {
//open specific panel
setDisplay(id)
}
}
Adjust the button's onClick
<button className="btnContainer" onClick={handleSetDisplay}>
Then to compare if your panel should expand, use
{display === id ? ///...: ...}
In short, you need to compare the saved ID with the mapped item's id.
If your id is a number, just change the initial state to 0
The buttons i create using below seems to lag in the selectedButtonIdx value.
Is the toggleSelected not complete by the time getClass is called ?
function ButtonGroup(props) {
const [selectedButtonIdx,setIdx]=useState(props.loadCurrentAsIndex);
const toggleSelected = (e) => {
setIdx(parseInt(e.target.dataset.index));
props.onclick(e);
};
const getClass = (index) => {
return (selectedButtonIdx === index) ? classnames('current', props.btnClass)
: classnames(props.btnClass)
};
let buttons = props.buttons.map((b, idx) => <Button key={idx} value={b.value} index={idx} text={b.text}
onclick={e => toggleSelected(e)}
btnClass={getClass(idx)}/>);
return (
<div>
{buttons}
</div>
);
}
Every onclick is expected to show the user which button in the group was clicked by changing its class.
By looking at this,
<Button
key={idx}
value={b.value}
index={idx}
text={b.text}
onclick={e => toggleSelected(e)}
btnClass={getClass(idx)}
/>
Button is your custom component,
Two things to notice here,
You have provided onclick (c is small) props, in you actual component it should be onClick={props.onclick}
You have used e.target.dataset.index, to work with dataset we should have attribute with data- prefix. So your index should be data-index in your actual component.
So finally your Button component should be,
const Button = (props) => {
return <button text={props.text} data-index={props.index} onClick={props.onclick} className={props.btnClass}>{props.value}</button>
}
Demo
The function setIdx, returned from useState is asynchronous, this means that it may be not be finished by the time you run your next function (as you guessed).
Take a look at useEffect it allows you to specify a function to run once an item in your state changes, this method will ensure your functions are called in the right order.
By now I don't see anything wrong here.
How it works:
initial render happens, onClick event listener is bound
user clicks a button, event handler calls setIdx triggering new render
new render is initiated, brand new selectedButtonIdx is used for rendering(and for getClass call as well)
See, there is no reason to worry about if setIdx is sync function or async.
I have a Main component with code
changeColor = (color) => {
}
toggle = (e) => {
console.log(e.target)
}
<div>
<EditComponent changeColor={this.changeColor.bind(this)}>
<TextComonent toggle={this.toggle.bind(this)}>
</div>
Edit component is
color = (value) => {
this.props.changeColor(value)
}
<div>
<button value='red' onClick={this.color.bind(this,"red")}>Red</button>
<button value='blue' onClick={this.color.bind(this,"blue")}>Blue</button>
</div>
Text component is
toggle = (e) => {
this.props.toggle(e)
}
<div>
<p class="black-color" onClick={this.toggle.bind(this)}>Text 1</p>
<p class="black-color" onClick={this.toggle.bind(this)}>Text 2</p>
</div>
I will be clicking on Text 1 or Text 2 first and I will get the event inside toggle function. Next I will click the button Red or Blue. Then I want to change the class to either red-color or blue-color for that particular Text that i have clicked before. How can I get the event inside the parent component to find the particular text or is there any other way to to this?
I want to get the event.target inside the Parent component. I got the event object in parent but event.target is null
<div>
<EditComponent changeColor={this.changeColor.bind(this)}>
<TextComonent toggle={this.toggle}>
</div>
try this way dont bind function in parent component and try,you will get the target
You are not using "bind" correctly. You don't need to bind with an anonymous function.
class Hello extends React.Component {
render() {
return (
<div>
<p onClick={(e) => this.toggle(e)}>
Test
</p>
</div>);
}
toggle = (e) => {
console.log(e.target.innerText);
}
}
From the event variable in toggle, you can perform your changes as need be.
I found the exact solution as to add event.persist(); to get the event.target inside parent component.
Just starting off with ReactJS and have a project where I am showing an accordion of issues and including a details area that is hidden on the start.
There is a button in the accordion bar that should pass a prop to the child element to hide or show them. I have refs on the button and on the details child compoment and added a function to call the function and pass the ref of the details area. I am just not sure how to dynamically change the class hidden on one of many areas and not all of them.
Not sure if putting a class on each element and then learning how to toggle the particular child's class is better or changing the prop to the child.
I can get to the change function but am drawing a blank from there and all the googling shows how to do one element with a grand change of state but I need individual elements.
Here is what I have so far.
Parent
...
<AccordionItem key={item.id} className={iconClass} title={`${item.area}`} expanded={item === 1}>
{
item.issues.map(issue => {
let trim = (issue.issue.length>21) ? `${issue.issue.substring(0,22)}...`: issue.issue;
return (
<div className="issue-bar container-fluid">
<div className="row issue-bar-row">
<span className="issue-title"><img src={CriticalRed} alt="Critical"/> {trim}</span>
<span className="btns">
<button className="btn btn-details" onClick={() => this.showDetail(`details-${issue.id}`)}>Details</button>
</span>
</div>
<IssuesDetails ref={`details-${issue.id}`} issue={issue} shouldHide={true} />
</div>
)
})
}
<div>
</div>
</AccordionItem>
...
Child
export default class IssuesDetails extends Component{
render(){
let issueDetails = classNames( 'issue-details', { hidden: this.props.shouldHide } )
return(
<div className={issueDetails}>
<div className="issues-details-title">
<h3>{this.props.issue.issue}</h3>
</div>
<div className="issues-details-details">
{this.props.issue.details}
</div>
<div className="issues-details-gallery">
<ImageGallery source={this.props.issue.photos} showPlayButton={false} useBrowserFullscreen={false} />
</div>
<button className="btn btn-success">Resolve</button>
</div>
)
}
}
Thanks for any help you provide or places you can send me!
If i'm understanding correctly, you need to be able to swap out shouldHide={true} in certain circumstances. To do this, you'll want your parent component to have a state object which indicates whether they should be hidden or not.
Exactly what this state object looks like depends on what sort of data you're working with. If the issues is a single array, then perhaps the state could be an array of booleans indicating whether each issue is expanded or not. I suspect you may have a more nested data structure, but i can't tell exactly since some of the code was omitted.
So assuming you have an array, it might look like this (i've omitted some things from the render method for brevity):
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
hidden: (new Array(props.issues.length)).fill(false),
};
}
showDetail(index) {
let newHidden = this.state.hidden.slice();
newHidden[index] = true;
this.setState({
hidden: newHidden
});
}
render() {
return (
<AccordionItem>
{this.props.issues.map((issue, index) => {
<div>
<button onClick={() => this.showDetail(index))}/>
<IssuesDetails issue={issue} shouldHide={this.state.hidden[index]}/>
</div>
})}
</AccordionItem>
);
}
}
Take a look at these:
https://codepen.io/JanickFischr/pen/xWEZOG
style={{display: this.props.display}}
I think it will help with your problem. If you need more information, please just ask.
I'm using ReactJS and ES2015
I pass a button via props down into a child component.
I can't see how to get data from the child into the onClick function of the button
Can anyone suggest what I need to do to get data from the child component into the onCLick function?
doDeleteItem = (blah) => {
console.log('the item to delete is: ', blah);
};
deleteButton = (
<button
className="btn btn-expand btn-stroke btn-success mr-5"
type="primary"
onClick={this.doDeleteItem}Delete item
</button>
)
render() {
return (
<TableContainer
deleteButton={this.deleteButton}
doDeleteItem={this.doDeleteItem}
/>
);
UPDATE: the comments say it's a bit unclear.
Here's the context:
The TableContainer component displays rows of data.
I push Button components down into the TableContainer via props.
The TableContainer renders the buttons.
I also push a function down for the Button to call in its onClick event.
The idea being that the user selects rows in the table, they push the button (such as delete for example) and the button runs its onClick function which deletes the selected rows.
The problem is that I can't see how to get the data that defines the selected rows into the onClick function.
It appears that the "scope" of the onClick function is actually the parent component, not the TableContainer component, so it cannot see the variables the define which data rows the user has chosen to delete.
Is that more clear? Let me know if not. thanks
Try to define doDeleteItem, deleteButton as methods instead of properties, and then call deleteButton in child and pass arguments what do you need
doDeleteItem(rows) {
console.log('the item to delete is: ', rows)
};
deleteButton(rows) {
return <button
className="btn btn-expand btn-stroke btn-success mr-5"
type="primary"
onClick={ () => this.doDeleteItem(rows) }
>
Delete item
</button>
}
render() {
return (
<TableContainer
deleteButton={ this.deleteButton }
doDeleteItem={this.doDeleteItem}
/>
);
}
Example
You should just pass down the parent function to the child as a property and separate the button component from the parent completely. You can include this button component in the child component (although it would be best to separate this component from child also) and insert the parent function there. If you want to pass parameters, you need to bind the function.
Parent:
doDeleteItem = (blah) => {
console.log('the item to delete is: ', blah);
};
render() {
const locale = this.props.app.locale;
return (
<TableContainer
onButtonClick={this.doDeleteItem.bind(this, blah)}
doDeleteTableItem={this.doDeleteTableItem}
/>
);
Child (TableContainer):
render() {
var deleteButton = (
<button
onClick={this.props.onButtonClick}
</button>
)
return (
// render TableContainer with deleteButton ..
);
As #WitVault mentioned in his comment you could pass some reference from the onClick in deleteButton so that the table knows which one to delete.
deleteButton = (
<button
className="btn btn-expand btn-stroke btn-success mr-5"
type="primary"
onClick={() => this.doDeleteItem(referenceToMeSoTableKnows)}>
Delete item
</button>
)
In your code deleteButton is getting called for just returing jsx. So you must be using it inside render method of TableContainer to render buttons.
and since here
<TableContainer
deleteButton={ this.deleteButton() }
doDeleteItem={this.doDeleteItem}
/>
doDeleteItem is passed as props inside TableContainer. you can access it via this.props inside TableContainer. Note this scope should be referring to scope of TableContainer so that you can access its props.
deleteButton = (
<button
className="btn btn-expand btn-stroke btn-success mr-5"
type="primary"
onClick={this.props.doDeleteItem.bind(this,item)}
</button>
)
I hope this should work.