Map creates only one li - React - reactjs

I'm making a component that creates Tabs when you click on an item in the navigation menu.
All the controls I do in the father to be able to pass the states between the brothers without problems.
As it is now my code when clicking on a menu item creates a unique <li> and shows the name. If you click on the entire menu item, it does not create a new <li>, but rather updates the old one with the new menu information.
I need that every time I press a menu item a new <li> with its content is created.
I edit my code with #technogeek1995 changes and this the final solution:
class App extends Component {
constructor(props, context){
super(props, context);
["openTabs",].forEach((method) => {
this[method] = this[method].bind(this);
});
this.state = {
navigation: {
menu: [],
},
tabs:{
tabsLi:[],
},
textvalue : "",
showtabs: true,
}
}
componentDidMount() {
fetch('json_menu.php')
.then(response => response.json())
.then(data =>{
this.setState({navigation: data});
//console.log(data)
})
}
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
const state = {...this.state};
state.textvalue = trdtitle.split();
state.tabs.tabsLi.push(state.textvalue);
console.log(state.tabs.tabsLi)
this.setState({ state });
this.setState({
showtabs: false,
});
}
class Tabs extends Component {
render(){
const renderTabs = tabs =>{
return(
<div id="content-tabs" className="tabs">
{( this.props.showtabs)
? (
<>
<div className="waiting-leads">
<p>Parece que todavía no hay ningún lead...</p>
<h3>¡Ánimo, ya llega!</h3>
<img src={imgDinosaurio} alt="Dinosaurio"></img>
</div>
</>
) : (
<ul id="resizable" className="content" >
{this.props.tabs.tabsLi.map((value, index) => {
return (
<li key={index}>
<span>{value}</span>
<Icon icon="cerrar" className='ico-cerrar' onClick={remove_tab(index)}/>
</li>
)
})}
</ul>
)}
</div>
);
}
return (
<>
{renderTabs(this.props.tabs.tabsLi)}
</>
)
}
}
This is the code that is generated when you click on the menu item. The <span> is the one that is updated and no new <li> is created. Ventas is my element menu name.
<ul id="resizable" class="content">
<li>
<span>Ventas</span>
<svg class="ico-cerrar">path</svg>
</li>
</ul>

The issue appears to be related to mutating the state directly. You should see some warnings in the console/terminal about mutating react's state directly. I have updatd your openTabs function so that it no longer mutates the state directly. Instead, I copy state to a local variable, perform the mutations of the local state object. Then, I call setState with the locally updated state object. React will automatically pick up the changes to the state and render the page with the (newly) updated state.
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
const state = {...this.state};
state.textvalue = trdtitle.split();
state.navigation.menu.push(state.textvalue);
state.showtabs = false;
this.setState({ state });
}
Tabs Component needed to be updated to iterate over the list, rather than over the string so it will create a <li> for every element in state.navigation.menu. remove_tab needed to be wrapped in {} instead of "" as well.
class Tabs extends Component {
render() {
return ( <
div id = "content-tabs"
className = "tabs" > {
(this.props.showTabs) ? (
<div className = "waiting-leads" >
<p> Parece que todavía no hay ningún lead... </p>
<h3> ¡Ánimo, ya llega! </h3>
<img src={imgDinosaurio} alt="Dinosaurio"/>
</div>
) : (
<ul id = "resizable" className = "content" >
{this.props.tabs.map((value, index) => (
<li key={index} >
<span>{value}</span>
<Icon icon = "cerrar" className = 'ico-cerrar' onClick={remove_tab(index)} / >
</li>
)
} </ul>
)} </div>
);
}
}

You should use the callback version of state and use the spread syntax to create new objects with new references so that React detects the change in state.
this.state.navigation.menu.push(this.state.textvalue)
Also, this line will push the old textValue and not the new one which is trdtitle.split()
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
const textValue = trdtitle.split()
this.setState(state => ({
textvalue,
showtabs: false,
navigation: {
...state.navigation,
menu: [ ...state.navigation.menu, textValue ]
}
}));
}

Related

Toggle one element in React.js onClick

I have info button that is supposed to open specific description element onclick event - info is obtained from Firebase. However, myOnclick event triggers all of the siblings elements and I need to toggle/untoggle only specific one. What am I missing and doing wrong?
here's the code:
import React, { Component } from "react";
import firebase from "../../firebase";
//Data obtained from DB and rendered on page
export default class Tour extends Component {
constructor() {
super();
this.state = {
tours: [],
showInfo: false,
};
}
// button that toggles info
handleInfo = () => {
this.setState({
showInfo: !this.state.showInfo,
});
};
// component did mount
componentDidMount() {
const dbRef = firebase.database().ref();
dbRef.on("value", (snapshot) => {
// checking changes in db
const data = snapshot.val();
const newToursAarray = [];
for (let inventoryName in data) {
const toursObject = {
id: inventoryName,
tours: data[inventoryName],
name: data[inventoryName].name,
seats: data[inventoryName].seats,
date: data[inventoryName].date,
duration: data[inventoryName].duration,
imgUrl:"https://source.unsplash.com/350x350/?" + data[inventoryName].name,
// temporary tour info placeholder and will be removed and connetcted to real DB
info: "Lorem ipsum dolora saepe fugiat. " +
data[inventoryName].name,
};
newToursAarray.push(toursObject);
}
this.setState({
tours: newToursAarray,
});
});
}
render() {
return (
<div className="tourlist">
{this.state.tours.map((toursObject) => {
return (
<section className="tourItem">
<header>
<h3> {toursObject.name} </h3>
<h5>
info
{/* button that toggles info */}
<span onClick={this.handleInfo}>
<i className="fas fa-caret-square-down"></i>
</span>
</h5>
</header>
<ul className="inventoryItem" key={toursObject.id}>
<li> {toursObject.date} |</li>
<li>{toursObject.duration} hrs |</li>
<li> {toursObject.seats} seats </li>
</ul>
<div className="img-container">
{this.state.showInfo && (
// text that toggles when clicking on info button
<p className="tour-info">{toursObject.info}</p>
)}
<img src={toursObject.imgUrl} alt="image of the tour" />
</div>
</section>
);
})}
</div>
);
}
}
So the issue here is that the expanding options are all referencing the same piece of state. You have your state.showInfo outside of your tours.map. So as you map through your tours, you say "if 'showInfo' then reveal this section" and that 'showInfo' is the same for everything.
What I would recommend is storing the id of the expanded tourObject, and then you can check against that.
Something like this:
handleInfo = (id) => {
this.setState({
infoToShow: id,
});
};
And then in your onClick it would look more like this:
<span onClick={() => this.handleInfo(tourObject.id)}>
<i className="fas fa-caret-square-down"></i>
</span>
And your logic to show or hide can just be this:
{this.state.infoToShow === tourObject.id && (
// text that toggles when clicking on info button
<p className="tour-info">{toursObject.info}</p>
)}
This way you can store the id in a place accessible to all the looped through tours, but they won't conflict with each other.
So instead of checking if showInfo is true, check if the id that you want to show matches the id of the tour.

react custom dropdown not updating state

Trying to create a custom styled dropdown without using select. I'm trying to update the state value based on the click, but it still reads the value of 'choose". Any ideas on how to update this so the select works?
import React from "react"
import styles from "./style.module.less"
class DropDown extends React.Component {
constructor(props) {
super(props)
this.state = {
isActive: false,
value: 'CHOOSE'
}
this.buttonRef = React.createRef()
this.menuRef = React.createRef()
}
toggleState = (e) => {
let {name, value} = e.target;
this.setState({
[name]: value,
});
}
toggleMenu() {
console.log(this.state.isActive)
this.setState(prevState => ({ isActive: !prevState.isActive }))
}
render() {
return (
<div
className={styles.dropdown}
onClick={() => this.toggleMenu()}
>
<div className={styles.toggle} ref={this.buttonRef}>
{this.state.value}
<b className={styles.rotate}>+</b>
</div>
{this.state.isActive && (
<ul className={styles.menu} ref={this.menuRef}>
<li className={styles.menuItem} value="a" onChange={this.toggleState}>a</li>
<li className={styles.menuItem} value="b" onChange={this.toggleState}>b</li>
<li className={styles.menuItem} value="c" onChange={this.toggleState}>c</li>
<li className={styles.menuItem} value="d" onChange={this.toggleState}>d</li>
<li className={styles.menuItem} value="e" onChange={this.toggleState}>e</li>
<li className={styles.menuItem} value="f" onChange={this.toggleState}>f</li>
</ul>
)}
</div>
)
}
}
export default DropDown
Couple of things
I see you have refs for button and menu but don't see them being
used.
The issue was that there was event bubbling happening. Your div
onClick which was toggling the menu was getting bubbled so the event
from li was never really getting called.
I wrapped your jsx with a fragment, you want your conditional render of the ul outside of the div that is toggling the menu, so that no event bubbling occurs.
I changed the onChange to onClicks.
Event.target.value will not work on li even if you give the attribute value. so now im just passing it directly to the toggleState method.
You may want to put the values [a,b,c] in an array and map through it.
I removed the styles just to make it work on my local, you can put them back.
but here you go
import React from "react";
class DropDown extends React.Component {
constructor(props) {
super(props);
this.state = {
isActive: false,
value: "CHOOSE"
};
this.buttonRef = React.createRef();
this.menuRef = React.createRef();
}
toggleState = value => {
this.setState({
value
});
};
toggleMenu() {
this.setState(prevState => ({ isActive: !prevState.isActive }));
}
render() {
return (
<React.Fragment>
<div onClick={() => this.toggleMenu()}>
<div ref={this.buttonRef}>
{this.state.value}
<b>+</b>
</div>
</div>
{this.state.isActive && (
<ul ref={this.menuRef}>
<li onClick={() => this.toggleState("a")}>a</li>
<li onClick={() => this.toggleState("b")}>b</li>
<li onClick={() => this.toggleState("c")}>c</li>
<li onClick={() => this.toggleState("d")}>d</li>
<li onClick={() => this.toggleState("e")}>e</li>
<li onClick={() => this.toggleState("f")}>f</li>
</ul>
)}
</React.Fragment>
);
}
}
export default DropDown;

Child Not Re-rendering on List Item Deletion in Parent but Adding to the List Item Triggering Re-render

I have Simple Add and Delete to my List sample ..
I made two child components
Lead Form Component ( Which Add New Leads to the List )
Lead List Component ( Which Simply render Leads List also have delete button which trigger delete action by passing ID back to parent )
In parent , the.state.leads holds all the leads
on Form Submit .. it adds to the.state.leads and LEAD LIST CHILD Components
successfully Re-Renders with new added lead
but on deleting list in the LEAD LIST , The lead list not re renders
Image ; Dev Tool Debug in the browser -React Console screenshot ..
MY LeadList Component
.........................................................
class LeadList extends React.Component {
constructor(props) {
super(props);
this.state = {
leads: this.props.avlList
};
this.handelDeleteLead = this.handelDeleteLead.bind(this);
}
handelDeleteLead(e) {
e.preventDefault();
this.props.DeleteLead(e.target.id);
}
render() {
console.log(this.state.leads);
return (
<div>
<ul>
{this.state.leads.map(item => (
<li key={item.id}>
{item.name} - {item.mobile} -{item.active ? "Active" : "Inactive"}
-
<div
id={item.id}
onClick={this.handelDeleteLead}
cursor="pointer"
>
X
</div>
</li>
))}
</ul>
</div>
);
}
}
......
My APP.js Parent Componnet
....................................
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
leads: [{ id: 1, name: "Panindra", mobile: "88842555542", active: true }]
};
this.handleAddToLeads = this.handleAddToLeads.bind(this);
this.handleRemoveLeads = this.handleRemoveLeads.bind(this);
}
handleAddToLeads(lead) {
let newleadsTemp = this.state.leads;
lead.id = Math.random() * Math.random();
newleadsTemp.push(lead);
// assign a name of list to item list
let newLeads = newleadsTemp;
this.setState({
leads: newLeads
});
}
handleRemoveLeads(lead_id) {
console.log(" Leads list before fitler ..." + this.state.leads);
let newFitleredLeads = remove(this.state.leads, lead_id);
this.setState({
leads: newFitleredLeads
});
console.log(" Leads list after fitler ..." + this.state.leads);
}
render() {
return (
<div className="App">
<h1> My First Redux</h1>
<hr />
<div className="leadList">
<LeadList
avlList={this.state.leads}
DeleteLead={this.handleRemoveLeads}
/>
</div>
<div className="leadForm">
<LeadForm NewLead={this.handleAddToLeads} />
</div>
</div>
);
}
}
.....
I think the problem is that you use state in LeadList component. Try to remove state from LeadList component. You don't need to manage multiple state's (this is important).
class LeadList extends React.Component {
render() {
return (
<div>
<ul>
{this.props.avlList.map(item => (
<li key={item.id}>
{item.name} - {item.mobile} -{item.active ? "Active" : "Inactive"}
-
<div
id={item.id}
onClick={() => this.props.DeleteLead(item.id)}
cursor="pointer"
>
X
</div>
</li>
))}
</ul>
</div>
);
}
}
And fix handleRemoveLeads function in the parent (App) component.
handleRemoveLeads(lead_id) {
console.log(" Leads list before fitler ..." + this.state.leads);
// THIS IS NOT WORKING
//let newFitleredLeads = remove(this.state.leads, lead_id);
// BETTER SOLUTION
let newFitleredLeads = this.state.leads.filter(item => item.id !== lead_id);
this.setState({
leads: newFitleredLeads
});
console.log(" Leads list after fitler ..." + this.state.leads);
}
This should work fine.
Working example (without form): https://codesandbox.io/s/charming-kowalevski-rj5nj

react-redux: Rendering a component after an API call

I am building an app which uses user input and shows number of recipes and they can click on recipe card to view ingredients as well. Every time they click on recipe card I make an API call to get appropriate recipe ingredient. But I am not able to figure out how to show the component which contains the recipe ingredients. I tried with conditional routing and conditional rendering as well but couldn't find the solution.
Recipe_Template.js
export class RecipeTemplate extends Component {
renderRecipe = recipeData => {
return recipeData.recipes.map(recipeName => {
return (
<div className="container">
<div className="row">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<a
href={recipeName.source_url}
target="_blank"
onClick={() => {
this.props.fetchRecipeId(recipeName.recipe_id);
}}
>
<img
src={recipeName.image_url}
className="mx-auto d-block img-fluid img-thumbnail"
alt={recipeName.title}
/>
<span>
<h3>{recipeName.title}</h3>
</span>
</a>
<span}>
<h3>{recipeName.publisher}</h3>
</span>
</div>
</div>
</div>
);
});
};
render() {
return (
<React.Fragment>
{this.props.recipe.map(this.renderRecipe)}
</React.Fragment>
);
}
}
Recipe_Detail.js
class RecipeDetail extends Component {
renderRecipeDetail(recipeData) {
return recipeData.recipe.ingredients.map(recipeIngredient => {
return <li key={recipeIngredient}>recipeIngredient</li>;
});
}
render() {
if (this.props.recipeId === null) {
return <div>Loading...</div>;
}
return <ul>{this.props.recipeId.map(this.renderRecipeDetail)}</ul>;
}
}
function mapStateToProps({ recipeId }) {
return { recipeId };
}
export default connect(mapStateToProps)(RecipeDetail);
Not entirely sure why you would need Redux here (unless it's being shared among other nested components), but I'm fairly certain you can just utilize React state.
One approach would be to configure your routes as such:
<Route path="/recipes" component={Recipes} />
<Route path="/recipe/:id" component={ShowRecipe} />
When the user sends a query, gets some results, and you display all matching recipes to a Recipes component. Each recipe then has a name (and other associated displayable data) and a clickable link:
<Link to={`/recipe/id?recipeId=${recipeId}`}>View {recipeName} Recipe</Link>
which for simplicity sake might look like:
<ul>
<Link to="/recipe/id?recipeId=08861626">View Prosciutto Bruschetta Recipe</Link>
<Link to="/recipe/id?recipeId=04326743">View Pasta Bundt Loaf Recipe</Link>
...etc
</ul>
When the user clicks on the link, react-router sends the user to the ShowRecipe component with a unique recipeId.
ShowRecipe then makes another AJAX request to get the recipe details:
ShowRecipe.js
export default class ShowRecipe extends Component {
state = { recipeDetail: '' }
componentDidMount = () => {
const { recipeId } = this.props.location.query; // <== only natively available in react-router v3
fetch(`http://someAPI/recipe/id?recipeId=${recipeId}`)
.then(response => response.json())
.then(json => this.setState({ recipeDetail: json }));
}
render = () => (
!this.state.recipeDetails
? <div>Loading...</div>
: <ul>
{this.state.recipeDetail.map(ingredient => (
<li key={ingredient}>ingredient</li>
)}
</ul>
)
}
Another approach:
Have the recipeDetails stored and available within the original fetched recipes JSON. Then map over the recipes and create multiple <Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} /> components for each recipe.
which for simplicity sake might look like:
<div>
{this.state.recipes.map(({recipeId, recipeName, recipeDetail}), => (
<Card key={recipeId} recipeName={recipeName} recipeDetail={recipeDetail} />
)}
</div>
Then each individual Card has it's own state:
Card.js
export default class Card extends Component {
state = { showDetails: '' }
toggleShowDetails = () => this.setState(prevState => ({ showDetails: !this.state.showDetails }))
render = () => (
<div>
<h1>{this.props.recipeName} Recipe</h1>
<button onClick={toggleShowDetails}> {`${!this.state.showDetails ? "Show" : "Hide"} Recipe<button>
{ this.state.showDetails &&
<ul>
{this.props.recipeDetail.map(ingredient => (
<li key={ingredient}>ingredient</li>
)}
</ul>
}
)
}
Therefore, by default the recipeDetail is already there, but hidden. However, when a user clicks the Card's button, it will toggle the Card's showDetails state to true/false to display/hide the recipe detail.

Passing state back to child component

I'm trying to figure out how can i properly pass state back to the child component.
Currently I have list of items and everytime i click on one of the items it changes state of "selectedVideo" variable in parent component. And then I would like to add class to the item that corresponds to that state in that child component. Basically when I click on that item in that list it become highlighted because it just changed the state of parent component.
So the main parent component is here:
index.js
class App extends Component {
constructor(props) {
super(props)
this.state = {
videos2:[],
selectedVideo:null
}
this.DMSearch()
}
DMSearch(term){
fetch(`https://api.dailymotion.com/videos?fields=description,id,thumbnail_60_url,title,url,&limit=5&search=${term}`)
.then(result => result.json())
.then(videos2 => {
//console.log(videos2.list[0]);
this.setState({
videos2: videos2.list,
selectedVideo: videos2.list[0]
});
//console.log(this.state.selectedVideo);
});
}
render () {
const DMSearch = _.debounce((term) => { this.DMSearch(term)}, 400);
return (
<div>
<SearchBar onSearchTermChange= {DMSearch}/>
<VideoDetail video={this.state.selectedVideo}/>
<VideoList
onVideoSelect={selectedVideo=>this.setState({selectedVideo})}
videos2={this.state.videos2}/>
</div>
)
}
}
Now the child component which changes state onclick
video_list_item.js
const VideoListItem = ({video, onVideoSelect}) => {
const imageUrl = video.thumbnail_60_url;
return (
<li onClick={() => onVideoSelect(video)} className="list-group-item">
<div className="video-list media">
<div className="media-left">
<img className="media-obj" src={imageUrl}/>
</div>
<div className="media-body">
<div className="media-heading">{video.title}</div>
</div>
</div>
</li>
);
};
And what I want is to add class "active" to this specific line
<li onClick={() => onVideoSelect(video)} className="list-group-item">
Based on the state of selectedVideo that changed in index.js after clicking on that component.
Also here is the code for the whole list.
video_list.js
const VideoList = (props) => {
const videoItems = props.videos2.map((video)=>{
return (
<VideoListItem
onVideoSelect={props.onVideoSelect}
key={video.id}
video={video} />
)
})
return (
<ul className="col-md-4 list-group">
{videoItems}
</ul>
)
}
You have to pass the selectedVideo state of your App to the VideoList component,
<VideoList
videos2={this.state.videos2}
onVideoSelect={selectedVideo=>this.setState({selectedVideo})}
selectedVideo={this.state.selectedVideo}
/>
which in turn passes it to each VideoListItem
const videoItems = props.videos2.map((video)=>{
return (
<VideoListItem
onVideoSelect={props.onVideoSelect}
key={video.id}
video={video}
active={video === props.selectedVideo}
/>
)
})
so each item can compare itself to the selectedVideo and display an 'active' class if needed.

Resources