Dynamically displaying data from a clickable table row into a modal - javascript

I'm attempting to create a component that consists of rows of data, which when clicked, open a modal with information relating to that table row. For example, when a user clicks on "team 1", a modal would appear showing a new table displaying each of the users assigned to that team.
I've managed to achieve this using manually provided parameters, however I have no idea how to make the modal dynamically display data depending on which table row has been clicked. Here is a link to a jsfiddle that i've made to show my problem.
getInitialState: function () {
return {
teams:[
{
id: '1',
teamName: 'team 1',
users: ['dave', 'steve', 'jim', 'barry', 'tom', 'harry']
},
]
};
render: function () {
var self = this;
var projectsTable = this.state.teams.map(function (obj, index) {
return (
<tr className="table-teamProject" key={index} data-toggle="modal" data-target="#projectUsersModal" data-id='3'>
<div className="mCellsContainer">
<div className="mCellsNames">{obj.teamName}</div>
<div className="mCellsCount">{obj.users.length} Users</div>
</div>
</tr>
);
});
var projectUsersModal = this.state.teams.map(function (obj, index) {
return (
<div className="modal projectUsersModal fade" id="projectUsersModal" tabIndex={-1} role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
</div>
</div>
</div>
);
});
return (
<div>
<div className="projectsColContainer">
<div className="panel panel-default">
<div className="panel-heading">Projects</div>
<table className="scroll-table">
{projectsTable}
{projectUsersModal}
</table>
</div>
</div>
</div>
);
}

The render() method is creating, what I think would be, a hidden modal for every team you have in your teams array, regardless of if the user requested the modal to show up (clicked on the team's link) or not. A better approach would be to create the specific modal on demand, that's when the user clicks on the team's link.
This can be done by creating a click handler and inside that function you would modify the state by setting the id of the team the modal is about, like so:
onClickTeam: function(teamId) {
this.setState({
openModalTeamId: this.state.openModalTeamId == teamId ? null : teamId
});
}
Then in your render() method you will want to check if this openModalTeamId state property has some value in it, if so and since your are storing the team's id in there, you would want to look for this particular team in your state teams array using the Array.prototype.find and then use the returned result to construct your modal's content.
render: function() {
...
var modalBody;
if (this.state.openModalTeamId) {
var team = this.state.teams.find(function(el) {
return el.id == self.state.openModalTeamId
});
modalBody =
...
<div className="modal-body">
Lets assume this is your modal containing the
following info about the selected team:
<br /><br />
{JSON.stringify(team)}
<br /><br />
<div onClick={(this.onClickTeam.bind(this, team.id))}>
Click me to close
</div>
</div>
...
}
...
}
Once you have that you can just append this new modalBody variable to your render's JSX just like you do in your code using the projectUsersModal variable. If no team was clicked on, then this variable would be undefined and no modal will show up.
return (
<div>
<div className="projectsColContainer">
<table className="scroll-table">
{projectsTable}
{modalBody}
</table>
</div>
</div>
);
jsFiddle

You can use https://github.com/fckt/react-layer-stack .
It allows you to both use variables from closure (which will propagate automatically if you'll provide it to "use" property of Layer) and also set event data from your toggle to modal window. Also you can have "stack" of layers with zIndex, one on another.
import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
return (
<Cell {...props}>
// the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
<Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
hideMe, // alias for `hide(modalId)`
index } // useful to know to set zIndex, for example
, e) => // access to the arguments (click event data in this example)
<Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
<ConfirmationDialog
title={ 'Delete' }
message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
confirmButton={ <Button type="primary">DELETE</Button> }
onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
close={ hideMe } />
</Modal> }
</Layer>
// this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
<LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
<div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
<Icon type="trash" />
</div> }
</LayerContext>
</Cell>)
// ...

Related

How can I display an HTML button using the return from a javascript function?

I trying to have my function return HTML button tags that will be displayed on the page like so:
(without the hover thing)
The problem is that as you can see every row has different buttons, so I wanted to make a function that takes in the parameter the "category" of the row, for example the "Games" row has 2 buttons: Modify and Delete.
Here's my javascript code:
function Category(cat) {
if (cat == "games") {
let innerhtml = `<div className='game_buttons'> <button className='modify'>Modify</button> <button className='delete'>Delete</button> </div>`
return innerhtml
}
if (cat == "upcoming") {
let innerhtml = `<div className='game_buttons'> <button className='released'>Released</button> <button className='modify'>Modify</button> <button className='delete'>Delete</button> </div>`
return innerhtml
}
if (cat == "featured") {
let innerhtml = `<div className='game_buttons'> <button className='delete'>Delete</button> </div>`
return innerhtml
}
}
I placed that same function where I need it to be displayed, like so :
<div className='game_buttons'>
{Category("games")}
</div>
But I get this :
So how could I display this buttons instead of just getting the HTML tags as text?
React purposefully makes injecting HTML strings cumbersome to try and steer you towards "thinking in React."
Here, this means just creating another component that conditionally renders the content you want as JSX.
function Category(props) {
if (props.cat === "games") {
return (
<div className="game_buttons">
<button className="modify">Modify</button>
<button className="delete">Delete</button>
</div>
);
}
if (props.cat === "upcoming") {
return (
<div className="game_buttons">
<button className="released">Released</button>
<button className="modify">Modify</button>
<button className="delete">Delete</button>
</div>
);
}
if (props.cat === "featured") {
return (
<div className="game_buttons">
<button className="delete">Delete</button>
</div>
);
}
}
Then, render Category and pass in your cat prop:
<Category cat="games" />

Get element of an array on click

I'm doing a small project where I show informations about every country in the world via an API
Where I'm struggling here is that whenever I click on a card I want to show a modal with more informations about the country
This part works nicely, BUT whatever the card I click on it's always going to be the last element of the array informations that I'm going to get
For exemple, If I click on "Afghanistan":
I get the last element of the array (the array being every card), which is Zimbabwe
This is my code
Basically the "country" parameter is the API data
What I'm doing is looping through every card and adding an event listener on click and trying to get all the informations about the card in the modal... but it doesn't work, I only get the last card infos
function showModal(country) {
const cards = document.querySelectorAll(".card");
for (card of cards) {
card.addEventListener("click", (e) => {
openModal.classList.toggle("active");
openModal.innerHTML = `
<div id="modal">
<div id="close-modal">
<i class="gg-close"></i></i>
</div>
<div id="modal-img">
<img src="${country.flag}">
</div>
<div id="modal-content">
<div class="country-details">
<h2>${country.name}</h2>
<h4>Nom natif : <span>${country.nativeName}</span></h4>
<h4>Population : <span>${country.population.toLocaleString()}</span></h4>
<h4>Région : <span>${country.region}</span></h4>
<h4>Sous-région : <span>${country.subregion}</span></h4>
<h4>Capitale : <span>${country.capital}</span></h4>
</div>
<div class="country-details">
<h4>Domaine de premier niveau : <span>${
country.topLevelDomain
}</span></h4>
<h4>Monnaie : <span>${country.currencies}</span></h4>
<h4>Langues : <span>${country.languages}</span></h4>
<h4>Fuseau horaire : <span>${country.timezones}</span></h4>
<h4>Indicatif téléphonique : <span>${
country.callingCodes
}</span></h4>
<h4>ISO 3166-1 alpha-3 : <span>${country.alpha3Code}</span></h4>
</div>
<div class="country-details">
<h4>Pays frontaliers</h4>
<span>${country.borders}</span>
</div>
</div>
</div>
`;
/* Close Modal */
const closeModalBtn = document.getElementsByClassName("gg-close");
closeModalBtn[0].addEventListener("click", (e) =>
e.target.parentElement.parentElement.parentElement.classList.remove(
"active"
)
);
});
}
}
I don't know much about arrays yet
Thanks!
The problem is that each time you call showModal, you are adding an event listener to all cards. Instead, you should add an event listener to the card corresponding to the country it received. That's why you should add some kind of id to each card, so you can tell which country goes in which card.
Assuming each card has an id equal to its country's name:
function showModal(country) {
const card = document.querySelectorAll(`#${country.name}`);
card.addEventListener("click", (e) => {
openModal.classList.toggle("active");
openModal.innerHTML = `
<div id="modal">
<div id="close-modal">
<i class="gg-close"></i></i>
</div>
<div id="modal-img">
<img src="${country.flag}">
</div>
<div id="modal-content">
<div class="country-details">
<h2>${country.name}</h2>
<h4>Nom natif : <span>${country.nativeName}</span></h4>
<h4>Population : <span>${country.population.toLocaleString()}</span></h4>
<h4>Région : <span>${country.region}</span></h4>
<h4>Sous-région : <span>${country.subregion}</span></h4>
<h4>Capitale : <span>${country.capital}</span></h4>
</div>
<div class="country-details">
<h4>Domaine de premier niveau : <span>${
country.topLevelDomain
}</span></h4>
<h4>Monnaie : <span>${country.currencies}</span></h4>
<h4>Langues : <span>${country.languages}</span></h4>
<h4>Fuseau horaire : <span>${country.timezones}</span></h4>
<h4>Indicatif téléphonique : <span>${
country.callingCodes
}</span></h4>
<h4>ISO 3166-1 alpha-3 : <span>${country.alpha3Code}</span></h4>
</div>
<div class="country-details">
<h4>Pays frontaliers</h4>
<span>${country.borders}</span>
</div>
</div>
</div>
`;
/* Close Modal */
const closeModalBtn = document.getElementsByClassName("gg-close");
closeModalBtn[0].addEventListener("click", (e) =>
e.target.parentElement.parentElement.parentElement.classList.remove(
"active"
)
);
});
}
By not having const or let in the for-loop card will be set globally and always hold the last the last value, since the "click" event is triggered asynchronously (after the loop is done iterating). Adding const or let to:
for (card of cards) {
Will scope card to the for-loop and let the callbacks refer to their respective value of card, instead of all callbacks referring to the last value of card.

React JSX for loop shows only the last value

I have seen similar questions here, but these haven't been helpful so far.
I have a component that has an array state:
eventData: []
Some logic watches for events and pushes the objects to the state array:
eventData.unshift(result.args);
this.setState({ eventData });;
unshift() here is used to push the new elements to the top of the array.
What I want to achieve is rendering the content of the state array. I have written a conditional that checks for a certain state, and based on that state decides what to output.
let allRecords;
if (this.state.allRecords) {
for (let i = 0; i < this.state.eventData.length; i++) {
(i => {
allRecords = (
<div className="event-result-table-container">
<div className="result-cell">
{this.state.eventData[i].paramOne}
</div>
<div className="result-cell">
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}}
</div>
<div className="result-cell">
{this.state.eventData[i].paramThree.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFour.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{this.state.eventData[i].paramSix.c[0]}
</div>
</div>
);
}).call(this, i);
}
} else if (!this.state.allRecords) {
for (let i = 0; i < this.state.eventData.length; i++) {
if (this.state.account === this.state.eventData[i].paramOne) {
(i => {
allRecords = (
<div className="event-result-table-container">
<div className="result-cell">
{this.state.eventData[i].paramOne}
</div>
<div className="result-cell">
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}}
</div>
<div className="result-cell">
{this.state.eventData[i].paramThree.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFour.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{this.state.eventData[i].paramSix.c[0]}
</div>
</div>
);
}).call(this, i);
}
}
}
Problems that I have with this piece of code:
The code always renders the very last value of eventData state object.
I would like to limit the rendered elements to always show not more than 20 objects (the last 20 records of the state array).
paramTwo is a bool, and according to its value I expect to see either Win or Loose, but the field is empty (I get the bool value via the console.log, so I know the value is there)
Is this even the most effective way of achieving the needed? I was also thinking of mapping through the elements, but decided to stick with a for loop instead.
I would appreciate your help with this.
A few things :
First, as the comments above already pointed out, changing state without using setState goes against the way React works, the simplest solution to fix this would be to do the following :
this.setState(prevState => ({
eventData: [...prevState.eventData, result.args]
}));
The problem with your code here. Is that the arrow function was never called :
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}
}
This function can be reduced to the following (after applying the deconstructing seen in the below code) :
<span>{paramTwo ? 'Win' : 'Lose'}</span>
Next up, removing repetitions in your function by mapping it. By setting conditions at the right place and using ternaries, you can reduce your code to the following and directly include it the the JSX part of your render function :
render(){
return(
<div> //Could also be a fragment or anything
{this.state.allRecords || this.state.account === this.state.eventData[i].paramOne &&
this.state.eventData.map(({ paramOne, paramTwo, paramThree, paramFour, paramFive, paramSix }, i) =>
<div className="event-result-table-container" key={i}> //Don't forget the key like I just did before editing
<div className="result-cell">
{paramOne}
</div>
<div className="result-cell">
<span>{paramTwo ? 'Win' : 'Lose'}</span>
</div>
<div className="result-cell">
{paramThree.c[0]}
</div>
<div className="result-cell">
{paramFour.c[0]}
</div>
<div className="result-cell">
{paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{paramSix.c[0]}
</div>
</div>
)
}
</div>
)
}
Finally, to only get the 20 first elements of your array, use slice :
this.state.eventData.slice(0, 20).map(/* CODE ABOVE */)
EDIT :
Sorry, I made a mistake when understanding the condition you used in your rendering, here is the fixed version of the beginning of the code :
{this.state.allRecords &&
this.state.eventData.filter(data => this.state.account === data.paramOne).slice(0, 20).map(/* CODE ABOVE */)
Here, we are using filter to only use your array elements respecting a given condition.
EDIT 2 :
I just made another mistake, hopefully the last one. This should ahve the correct logic :
this.state.eventData.filter(data => this.state.allRecords || this.state.account === data.paramOne).slice(0, 20).map(/* CODE ABOVE */)
If this.state.allRecords is defined, it takes everything, and if not, it checks your condition.
I cleaned up and refactored your code a bit. I wrote a common function for the repetitive logic and passing the looped object to the common function to render it.
Use Map instead of forloops. You really need to check this this.state.account === this.state.eventObj.paramOne statement. This could be the reason why you see only one item on screen.
Please share some dummy data and the logic behind unshift part(never do it directly on state object), we'll fix it.
getRecord = (eventObj) => (
<React.Fragment>
<div className="result-cell">
{eventObj.paramOne}
</div>
<div className="result-cell">
{eventObj.paramTwo ? <span>Win</span> : <span>Loose</span>}
</div>
<div className="result-cell">
{eventObj.paramThree.c[0]}
</div>
<div className="result-cell">
{eventObj.paramFour.c[0]}
</div>
<div className="result-cell">
{eventObj.paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{eventObj.paramSix.c[0]}
</div>
</React.Fragment>
)
render() {
let allRecords;
if (this.state.allRecords) {
allRecords = <div>{this.state.eventData.map(eventObj => this.getRecord(eventObj)}</div>;
} else if (!this.state.allRecords) {
allRecords = <div>{this.state.eventData.map(eventObj => {
if (this.state.account === this.state.eventObj.paramOne) {
return this.getRecord(eventObj);
}
return null;
})}</div>;
}
return (<div className="event-result-table-container">{allRecords}</div>);
}

Error passing function onClick in react

I have two files. A list Component and a Single Item Component. In my app, the user can select multiples items. Then I create an state element in "list" "items" and my idea is that when the user make click on the item button, the list element notify to List Component and save the item in Items array from "list".
I have the next code
List.jsx:
registrarItems(data,item){
console.log(data,"aqui 1 con",item);
let items = this.state.itemsAgregados.slice();
if(!items.indexOf(data.id_producto)){
console.log("no se encuentra");
items.push(id);
this.setState({
'itemsAgregados':items
});
}else{
console.log("ya existe");
item.removerSeleccion();
}
console.log("registrando items",id);
}
render() {
return (
<div className="content-app">
<Navbar data={this.menu}/>
<div className="container lista-productos">
{
this.state.productos.map((producto, index) => {
return (
<Item data={producto}
registro = {this.registrarItems}
key={producto.id_producto}/>
);
})
}
</div>
</div>
);
}
And Item.jsx:
render() {
let props = this.props;
let img = JSON.parse(props.data.imagen);
let imgPath = Rutas.apiStatic + 'img/productos/' + props.data.id_producto + '/' + img.sm;
props.data.items = this;
return (
<div className="row item-listado">
<div className="col-xs-3">
<img src={imgPath} className="img-circle img-item"/>
</div>
<div className="col-xs-7">
<Link to={Rutas.producto + props.data.identificador}>
<h3 className="titulo">{props.data.titulo}</h3>
<span className="price">$ {props.data.precio}</span>
</Link>
</div>
<div className="col-xs-2 text-right">
<ul className="list-unstyled list-acciones">
<li>
<a href="#" onClick={()=>props.registro(props.data,this)} className={this.state.classAdd}>
<i className="fa fa-plus"></i>
</a>
</li>
</ul>
</div>
</div>
)
}
As you can see, I pass the "registrarItems" method as a param to Item and there, i add this as onClick event in the tag from item. But I need pass the "data" and the own "item" element to the onclick function. The first, for save the element clicked in the Items array, or remove it (if it already exists) because the button may have a toggle function. But in my "console.log" both params passed on the onClick method with the arrow function shows as "undefined".
I saw some examples and i don't get my error. can anybody helpme? thanks.
The final solve for this was simple. I resolved it with something similar as Free-soul said on his comment.
First, I passed the List Component as a param to item. Below my code in List's render method:
{
this.state.productos.map((producto, index) => {
this.items[producto.id_producto] = producto;
return (
<Item data={producto}
parent = {this}
key={producto.id_producto}/>
);
})
}
Then I get the parent param in componentDidMount method and later I call the validarItem function directly from the List method and I pass the params that i need.
Here my Item code:
onClickPlus(id,data) {
//{Rutas.listas + 'productos/' + props.data.id_producto //Url para pasar uno solo
this.setState({
classAdd:'selected'
})
if(!this.state.parent.validarItem(this.state.data)){
this.removerSeleccion()
}
if(this.state.seleccionMultiple){
}
}
removerSeleccion(){
this.setState({classAdd:'normal'})
}
componentDidMount(){
this.setState({
parent: this.props.parent,
data : this.props.data
})
}
render() {
return (
// more code
<a href="#" onClick={() => this.onClickPlus(parent)} className={this.state.classAdd}>
<i className="fa fa-plus"></i>
</a>
//more render code...
)
}
I don't know if this is the best practice, but works for me.

ReactJs: target.key undefined after method is reached from li rendered by map function

I'm currently trying to coding a react app that would do the following:
- Create a list of questions from an array using a map function.
- Making each list element clickable using a onClick prop
- The linked onClick method changes the state in another file with my 'qsChange' prop.
I had a hard time making my list clickable and finally managed following this question: React: trying to add an onClick to a li tag but the click handler function is undefined
However, now I cannot make it so that my variable 'choice' returns a defined value. I would want var choice to be equal to "A ?", "B ?" or "C ?" depending on which I click.
Here's my code:
var questions = ["A ?", "B ?", "C ?"];
var Questions = React.createClass({
handleClick: function() {
var visibility;
if(this.props.visibility) {
document.getElementById('second').style.display = 'none';
visibility = false;
this.props.onChange(visibility);
} else {
document.getElementById('second').style.display = 'block';
visibility = true;
this.props.onChange(visibility);
}
},
/* Here is where my problem lies */
onItemClick: function(e){
var choice = e.target.key;
this.props.qsChange(choice);
alert(choice);
},
render: function() {
return (
<div className="bigqs">
<div id="first" className="small" style={firstStyle}>
<h1>Question :</h1>
<button style={btnStyle} onClick={this.handleClick}>
<img id="arrow" src="../../img/arrow.png" />
</button>
<h3 id="selectedQuestion">{this.props.selected}</h3>
</div>
<div id="second" className="small" style={{display: 'none'}}>
<h4>
<ul>
{questions.map(function(qs, i) {return <li key={qs[i]} onClick={this.onItemClick}>{qs}</li>;}, this)}
</ul>
</h4>
</div>
</div>
);
}
});
I am still a newbie, so please be indulgent ;-)
I hope I was clear enough.
Ps: I have also tried this guide but it didn't work for me: http://derpturkey.com/react-pass-value-with-onclick/
Instead of grabbing the question from target, you can pass question through to your handler. Also, since inside map qs is a string, qs[i] will be getting the character in the string from that index. You just need to make sure your key is unique.
onItemClick: function(choice) {
this.props.qsChange(choice)
alert(choice)
},
render() {
return (
<div>
...
{questions.map(qs =>
<li key={qs} onClick={() => this.onItemClick(qs)}>{qs}</li>
)}
...
</div>
)
}
In fact, your intermediate function isn't doing much, you can just call your props function inside render:
render() {
return (
<div>
...
{questions.map(qs =>
<li key={qs} onClick={() => this.props.qsChange(qs)}>{qs}</li>
)}
...
</div>
)
}

Categories