react expand and collapse just one panel - javascript

Need help with react...
Trying to implement a collapsible list of cards with weather information.
Already implemented the behavior of expand and collapse, but when i clicked on one panel the other panel open at the same time (i have 2 panels and need 7 to display weahter for 7 days of the week).
How can i open and close just one panel?
Code:
import React, { Component } from 'react';
import Moment from 'react-moment';
import RandomGif from './RandomGif.js';
const urlForCity = city => `https://cors-anywhere.herokuapp.com/http://api.openweathermap.org/data/2.5/forecast/daily?q=${city}&units=metric&cnt=7&appid=1fba7c3eaa869008374898c6a606fe3e`
class OpenWapi extends Component {
constructor(props) {
super(props);
this.state = {
requestFailed: false,
shown: false
}
this.componentDidMount = this.componentDidMount.bind(this);
this.toggle = this.toggle.bind(this);
}
componentDidMount() {
fetch(urlForCity(this.props.city))
.then(response => {
if(!response.ok) {
throw Error("Network request failed")
}
return response;
})
.then(data => data.json())
.then(data => {
this.setState({
weatherData: data
})
}, () => {
this.setState({
requestFailed: true
})
})
}
toggle() {
this.setState({
shown: !this.state.shown
});
}
render() {
if(this.state.requestFailed) return <p>Request Failed.</p>;
if(!this.state.weatherData) return <p>Loading...</p>;
return (
<div>
<p>City: {this.state.weatherData.city.name}</p>
{/* Day 1 */}
<div onClick={this.toggle} className="dayWeekItem">
<div className="top-content">
<div className="icon-weather"></div>
<div className="date">
<div className="weekday">Today</div>
<div className="day-long"><Moment unix format="MMM DD YYYY">{this.state.weatherData.list[0].dt}</Moment></div>
</div>
<div className="temperature">
<div className="temp-high">{parseInt(this.state.weatherData.list[0].temp.max)}º</div>
<div className="temp-low">{parseInt(this.state.weatherData.list[0].temp.min)}º</div>
</div>
</div>
<div className={this.state.shown ? "toggleContent-open" : "toggleContent-closed"} >
<div className="weather-gif" >
<RandomGif keyword={this.state.weatherData.list[0].weather[0].description} />
</div>
</div>
</div>
{/* Day 2 */}
<div onClick={this.toggle} className="dayWeekItem">
<div className="top-content">
<div className="icon-weather"></div>
<div className="date">
<div className="weekday">Tomorrow</div>
<div className="day-long"><Moment unix format="MMM DD YYYY">{this.state.weatherData.list[1].dt}</Moment></div>
</div>
<div className="temperature">
<div className="temp-high">{parseInt(this.state.weatherData.list[1].temp.max)}º</div>
<div className="temp-low">{parseInt(this.state.weatherData.list[1].temp.min)}º</div>
</div>
</div>
<div className={this.state.shown ? "toggleContent-open" : "toggleContent-closed"} >
<div className="weather-gif" >
<RandomGif keyword={this.state.weatherData.list[1].weather[0].description} />
</div>
</div>
</div>
{/* Day 3 */}
{/* Day 4 */}
{/* Day 5 */}
</div>
)
}
}
export default OpenWapi;

I would have an object to represent the state, a field for each panel.
Like this:
constructor(props) {
...
this.state = {
requestFailed: false,
shown: {}
}
...
}
...
toggle(panelNumber) {
this.setState({
shown: {
...this.state.shown,
[panelNumber]: !this.state.shown[panelNumber]
}
});
}
...
The toogle function is used like this, for instance, Day 1:
<div onClick={() => this.toggle(1)} className="dayWeekItem">
...
</div>
And to show in html, for instance, Day 1:
<div className={this.state.shown[1] ? "toggleContent-open" : "toggleContent-closed"} >
<div className="weather-gif" >
<RandomGif keyword={this.state.weatherData.list[0].weather[0].description} />
</div>
</div>

They all will collapse always with your implementation.
You have a state
state = {
shown: true
}
You have a function to toggle it
toggle = () => {
this.setState(shown: !this.state.shown)
}
And you render the component, using the this.state.shown in two places, but the value will always be one true or false
render() {
return(<div .....//something>
<div onClick={this.toggle}>
{ this.state.shown ? <SomeComponent or HTML Tag> : null }
</div>
<div onClick={this.toggle}>
{ this.state.shown ? <SomeComponent or HTML Tag> : null }
</div>
</div>)
}
So where ever you toggle, once the state is updated and render method is called again to paint the view, both sections of divs get the sameBoolean` value. Therefore, they both collapse.
Best Solution I can offer for this problem will be:
Create a separate component which has two jobs to be do:
1. Maintains its own state, of collapse true or false.
2. Render the children given to it without wondering what they might be.
So let say
class WeatherWidget extends React.PureComponent {
state= {
shown: true
}
toggle = () => this.setState({shown: !this.state.shown})
render() {
return(
<div onClick={this.toggle} className="dayWeekItem">
<div className="top-content">
<div className="icon-weather"></div>
<div className="date">
<div className="weekday">Today</div>
<div className="day-long">
<Moment unix format="MMM DD YYYY">{this.props.date}</Moment>
</div>
</div>
<div className="temperature">
<div className="temp-high">{parseInt(this.props.maxTemp)}º
</div>
<div className="temp-low">{parseInt(this.props.minTemp)}º
</div>
</div>
</div>
<div className={this.state.shown ? "toggleContent-open" : "toggleContent-closed"} >
<div className="weather-gif" >
<RandomGif keyword={this.props.gifDescription} />
</div>
</div>
</div>
)
}
}
So you create a reusable component which manages its own state ( React Paradigm/ Composition brings reusability)
As for displaying multiple widgets
class OpenWapi extends Component {
constructor(props) {
super(props);
this.state = {
requestFailed: false,
shown: false
}
this.componentDidMount = this.componentDidMount.bind(this);
this.toggle = this.toggle.bind(this);
}
componentDidMount() {
fetch(urlForCity(this.props.city))
.then(response => {
if(!response.ok) {
throw Error("Network request failed")
}
return response;
})
.then(data => data.json())
.then(data => {
this.setState({
weatherData: data
})
}, () => {
this.setState({
requestFailed: true
})
})
}
render() {
if(this.state.requestFailed) return <p>Request Failed.</p>;
if(!this.state.weatherData) return <p>Loading...</p>;
return(
<div>
<p>City: {this.state.weatherData.city.name}</p>
<WeatherWidget
date={this.state.weatherData.list[0].dt}
maxTemp={this.state.weatherData.list[0].temp.max}
minTemp={this.state.weatherData.list[0].temp.min}
gifDescription=
{this.state.weatherData.list[0].weather[1].description}
/>
<WeatherWidget
date={this.state.weatherData.list[1].dt}
maxTemp={this.state.weatherData.list[1].temp.max}
minTemp={this.state.weatherData.list[1].temp.min}
gifDescription=
{this.state.weatherData.list[1].weather[1].description}
/>
</div>
)
}
Hopefully, this solves the use case.

Related

Hi ! React issues i need assistance with event handlers

I'm trying to make a components props(sideBarInfo) details show up on the left column of a Page after clicking on a corresponding component(thumbnail) on the right column of the same Page.
Please note that all imports and exports are used in the main project(i removed them here).
I've also imported all components into the main (Page.js). Yet i keep getting a Type error for the onClick.
This is the first component - Thumbnail
class Thumbnail extends React.Component {
render(){
return (
<div className="Work" onClick={(e) => this.props.click(this.props.work)} >
<div className="image-container">
<img src={this.props.work.imageSrc} alt={this.props.work.imageSrc}/>
</div>
<div className="Work-information">
<p> {this.props.work.work}</p>
</div>
</div>
);
} }
This is the ThumbnailList
class ThumbnailList extends React.Component {
constructor(props){
super(props);
this.state= {
works: [
{
id: 0,
work: 'Work 1',
imageSrc: W1,
view: '#',
selected: false
},
{
id: 1,
work: 'Work 2',
imageSrc: W2,
view: '#',
selected: false
},
]
}
}
handleCardClick = (id,card) => {
console.log(id);
let works= [...this.state.works];
works[id].selected = works[id].selected ? false : true ;
works.forEach(work=>{
if(work.id !== id){
work.selected = false;
}
});
this.setState({
works
})
}
makeWorks = (works) => {
return works.map(work => {
return <Thumbnail work={work} click={(e => this.handleCardClick(work.id,e ))} key={work.id} />
})
}
render(){
return(
<div>
<div className="scrolling-wrapper-flexbox">
{this.makeWorks(this.state.works)}
</div>
</div>
);
}
}
This is the sidebarInfo
function SidebarInfo(props) {
return (
<img width="370" height="370" src= {props.imageSrc} />
<p> {props.work} </p>
);}
This is the problematic Page - the boldened keeps giving a Type error(cannot read property 'selected' of undefined.)
class Page extends React.Component {
render() {
return (
<span>
<div className="column left">
<div className="">
**{this.props.work.selected && <SidebarInfo imageSrc={this.props.imageSrc} /> }**
</div>
</div>
<div className="column right" >
<div>
<ThumbnailList />
</div>
</div>
</span>
)
}
}
You may need to add a check in this.props.work.selected like this this.props.work && this.props.work.selected
To ensure that this.props.work is defined before checking on this.props.work.selected
I was able to answer this for anyone interested.
there were some foundational errors in my file arrangement which have been corrected.
After refactoring, i was able to achieve what i wanted using 3 classes/functions in; App.js, Thumbnail.js and SideBarInfo.js
see one working solution with styling on this sandbox: https://codesandbox.io/s/modern-sky-y9d3c
See Solution below;
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
works: [
{
id: 0,
work: "Work 1",
imageSrc:
"https://images.unsplash.com/photo-1584608168573-b6eec7a04fd7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=701&q=80",
view: "#",
selected: false
},
{
id: 1,
work: "Work 2",
imageSrc:
"https://images.unsplash.com/photo-1581665269479-57504728e479?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=701&q=80",
view: "#",
selected: false
}
]
};
this.handleCardClick = this.handleCardClick.bind(this);
this.makeWorks = this.makeWorks.bind(this);
this.showWorks = this.showWorks.bind(this);
}
handleCardClick = (id, Thumbnail) => {
console.log(id);
let works = [...this.state.works];
works[id].selected = works[id].selected ? false : true;
works.forEach((work) => {
if (work.id !== id) {
work.selected = false;
}
});
this.setState({
works
});
};
makeWorks = (works) => {
return works.map((work) => {
return (
<Thumbnail
work={work}
click={(e) => this.handleCardClick(work.id, e)}
key={work.id}
/>
);
});
};
showWorks = (works) => {
let i = 0;
var w = [];
while (i < works.length) {
if (works[i].selected) {
w = works[i];
}
i++;
}
return ( <SidebarInfo imageSrc={w.imageSrc} work={w.work} /> );
};
render() {
return (
<div className="App">
<span>
<div> { this.showWorks(this.state.works)} </div>
<div className="">
<div className="scrolling-wrapper-flexbox">
{this.makeWorks(this.state.works)}
</div>
</div>
</span>
</div>
);
}
}
class Thumbnail extends React.Component {
render() {
return (
<div>
<div className="column left">
{/* this.props.work.selected && <SidebarInfo work={this.props.work.work} imageSrc={this.props.work.imageSrc}/> */}
</div>
<div className="column right">
<div className="Work" onClick={(e) => this.props.click(this.props.work)}>
<div className="image-container">
<img src={this.props.work.imageSrc} alt={this.props.work.imageSrc} />
</div>
<div className="Work-information">
<p> {this.props.work.work}</p>
</div>
</div>
</div>
</div>
);
}
}
export default Thumbnail;
function SidebarInfo(props) {
return (
<div className="one">
<div className="Work">
<h1> NAME </h1>
<div className="image-container">
<img
width="370"
height="370"
src={props.imageSrc}
alt={props.imageSrc}
/>
</div>
<p> {props.work} </p>
<p> {props.view} </p>
</div>
</div>
);
}
export default SidebarInfo;

how fix selected permanently ReactJS

I would like to explain my problem of the day.
the code works correctly ,
the only problem is
when i click on my navbar to select a tab ,
my activeclassname works fine except that when i click elsewhere on the same page i lose my activeclassname
I would like him to be active permanently
Do you have an idea of how to fix this?
class MainHome extends Component {
constructor(props) {
super(props);
this.state = {
};
// preserve the initial state in a new object
this.toggle = this.toggle.bind(this);
this.cgtChange1= this.cgtChange1.bind(this);
this.cgtChange2= this.cgtChange2.bind(this);
}
cgtChange1() {
console.log('bière clicked')
this.setState({
cgt : 1
})
}
cgtChange2() {
console.log('cocktails clicked')
this.setState({
cgt :2
})
}
render() {
let addedItems = this.props.items.length ?
(
this.props.items.filter(item => item.ctg === this.state.cgt).map(item => {
return (
<div key={item.id}>
{item.title}
</div>
)
})) :
(
<div></div>
)
return (
<div>
<Header text='Carte'/>
<NavBar
onClick1={this.cgtChange1}
onClick2={this.cgtChange2}
/>
{addedItems}
</div>
)
}
}
NavBar
export default class FocusOnSelect extends Component {
render() {
return (
<div>
<button onClick={this.props.onClick1}
activeClasseName="selected" className='inactive' > Bières
</button>
</div>
<div >
<button onClick={this.props.onClick2} activeClasseName="selected" className='inactive' > Cocktails </button>
</div>
</div>
);
}
}

How to identify a single div element among set of elements in react?

I have a react app with a list of Div elements to create some Cards. Each card has 'read more' button to expand and collapse a paragraph and I toggle it for each mouse click. My problem is, for each click, it expand paragraphs in all cards instead only paragraph in the card I clicked. So I can't identify the clicked (this) card.
Component:
class BidCard extends Component {
constructor(props) {
super(props);
this.state = {
readMoreOpen: false,
}
}
readMore() {
this.setState({ readMoreOpen: !this.state.readMoreOpen })
}
render() {
const { articles } = this.props;
return (
articles.map(article => {
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={this.state.readMoreOpen ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={this.readMore.bind(this)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
export default BidCard;
How can I solve this?
You can save id of the expanded card to the state and the check it when rendering items:
class BidCard extends Component {
constructor(props) {
super(props);
this.state = {
readMoreOpen: [], // Use array here
}
}
// Add card id to the expanded array if not already there, otherwise remove it
readMore = (id) => {
this.setState(state => {
if (state.readMoreOpen.includes(id)) {
return {readMoreOpen: state.readMoreOpen.filter(readId => readId !== id)}
}
return {readMoreOpen: [...state.readMoreOpen, id]}
})
}
render() {
const { articles } = this.props;
return (
articles.map(article => {
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
{/*Check if the item is in expanded items array */}
<p className={this.state.readMoreOpen.includes(article.id) ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={() => this.readMore(article.id)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
You will need to keep expanded state per every card.
I would recommend to create component for card
articles.map(article => {
return (
<Article key={article.id} {...article} />
)
})
)
class Article extends Component {
state = {
readMoreOpen: false
}
readMore() {
this.setState(state => ({ readMoreOpen: !state.readMoreOpen }))
}
render () {
const {description} = this.props;
return (<div className="projectCardRoot" >
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={this.state.readMoreOpen ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{description}</p>
<div className="cardReadMore desktopDiv" onClick={this.readMore.bind(this)}>Read more</div>
</div>
</div>
</div>)
}
}
Other approach is to keep array of booleans with information of which article div should be currently expanded in this method you will need to update state with id of expanded article
readMore(id) {
this.setState({ articles: this.props.articles.map(article => article.id === id ? true : false) } )
}
and in render use boolean from state as information if it should be expanded
That's because all your cards currently share the same source of truth. You used a ternary operator to determine what class a Card would have depending on the state-value. Well, all Cards are using the same state-value to compare, so understandably, if one is affected, then all would be too.
There's more than one way to resolve this, but the most appropriate would probably be to create a separate Card Component. This makes it so each Card component has their own state to keep track of.
See working sandbox: https://codesandbox.io/s/quizzical-mahavira-wz8iu
Parent.js
import React from "react";
import ReactDOM from "react-dom";
import Card from "./Card";
import "./styles.css";
class BidCard extends React.Component {
render() {
const { articles } = this.props;
return articles.map(article => {
return <Card article={article} />;
});
}
}
BidCard.defaultProps = {
articles: [{ description: "woof" }, { description: "meow" }]
};
const rootElement = document.getElementById("root");
ReactDOM.render(<BidCard />, rootElement);
Card.js
import React, { useState } from "react";
const Card = ({ article }) => {
const [readOpen, setReadOpen] = useState(false);
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p
className={readOpen ? "openFullParagraph" : "closeFullParagraph"}
id="projectCardDesc"
>
{article.description}
</p>
<div
className="cardReadMore desktopDiv"
onClick={() => setReadOpen(!readOpen)}
>
Read more
</div>
</div>
</div>
</div>
);
};
export default Card;
I did a few modifications to your code. This way it should work.
I added comments that explain the the changes. The main idea is that you should not simply store the boolean readMoreOpen status (which in your code is treated as a kind of shared between all the cards) but specific card identity.
My changes works if there could be only one "expanded" card at any moment. If your design supposes that there could be a few "expanded" cards at the same time the solution would be more complex though not much.
class BidCard extends Component {
constructor(props) {
super(props);
// the way you've tried to keep status (open/closed) it wasn't tied to any speciifc card
// you should store this specific card instead
this.state = {
//readMoreOpen: false,
expandedCard: null,
}
this.readMore = this.readMore.bind(this);
}
readMore(article) {
//this.setState({ readMoreOpen: !this.state.readMoreOpen })
this.setState({expandedCard: article})
}
render() {
const { articles } = this.props;
const { expandedCard } = this.state;
return (
articles.map(article => {
// the look of each card depends on state.expandedCard only if article == expandedCard it's shown with 'openFullParagraph' class
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={article == expandedCard ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={() => this.readMore(article)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
export default BidCard;

this.someFunction is not a function

After having read about the bind requirement for methods to be bound to a React ES6 class, I am still having some difficulty with this example:
class ProductList extends React.Component {
constructor(props) {
super(props);
this.state = { products: [] };
this.updateState = this.updateState.bind(this);
}
componentDidMount() {
this.updateState();
}
handleProductUpvote(productId) {
Data.forEach((item) => {
if (item.id === productId) {
item.votes = item.votes + 1;
return;
}
});
this.updateState();
}
updateState() {
const products = Data.sort((a,b) => {
return b.votes - a.votes;
});
this.setState({ products });
}
render() {
const products = this.state.products.map((product) => {
return (
<Product
key={'product-' + product.id}
id={product.id}
title={product.title}
description={product.description}
url={product.url}
votes={product.votes}
submitter_avatar_url={product.submitter_avatar_url}
product_image_url={product.product_image_url}
onVote={this.handleProductUpvote}
/>
);
});
return (
<div className='ui items'>
{products}
</div>
);
}
}
class Product extends React.Component {
constructor() {
super();
this.handleUpvote = this.handleUpvote.bind(this);
}
handleUpvote() {
this.props.onVote(this.props.id);
}
render() {
return (
<div className='item'>
<div className='image'>
<img src={this.props.product_image_url} />
</div>
<div className='middle aligned content'>
<div className='header'>
<a onClick={this.handleUpvote}>
<i className='large caret up icon'></i>
</a>
{this.props.votes}
</div>
<div className='description'>
<a href={this.props.url}>
{this.props.title}
</a>
</div>
<div className='extra'>
<span>Submitted by:</span>
<img
className='ui avatar image'
src={this.props.submitter_avatar_url}
/>
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<ProductList />,
document.getElementById('content')
);
This returns
Uncaught TypeError: this.updateState is not a function(...) at handleProductUpvote
Is the initialized binding not sufficient in this case?
Whenever you see this issue, you don't want to be adding the bind to the method that it's trying to call right then, but the method that you are inside of when the "this.xxx not defined" issue occurs.
Currently, it's getting the function handleProductUpvote just fine - but it's calling it in the wrong object context. So you need to do the same thing as you did with updateState in the constructor, but with that function. Though I have limited react knowledge I believe it's common to do that for every function that's used as an event listener or callback.

react show button on mouse enter

I have a react component which hold method like:
mouseEnter(){
console.log("this is mouse enter")
}
render(){
var album_list;
const {albums} = this.props
if(albums.user_info){
album_list = albums.user_info.albums.data.filter(album => album.photos).map((album => {
return
<div className={"col-sm-3"} key={album.id} onMouseEnter={this.mouseEnter}>
<div className={(this.state.id === album.id) ? 'panel panel-default active-album' : 'panel panel-default'} key={album.id} onClick={this.handleClick.bind(this, album.id)}>
<div className={"panel-heading"}>{ album.name }</div>
<div className={"panel-body"}>
<img className={"img-responsive center-block"} src={album.photos.data[0].source} />
</div>
</div>
</div>
}))
}
return (
<div className={"container"}>
<div className="row">
{album_list}
</div>
</div>
)
}
}
Here I have onMouseEnter on album_list. When it is hover or mouse enter I want to dispalay a button on that div.
How can I do that ??
Thank you
Update the component's state to reflect whether the mouse is inside the component, then use the state value to conditionally render a button.
getInitialState() {
return {
isMouseInside: false
};
}
mouseEnter = () => {
this.setState({ isMouseInside: true });
}
mouseLeave = () => {
this.setState({ isMouseInside: false });
}
render() {
return (
<div onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave}>
{this.state.isMouseInside ? <button>Your Button</button> : null}
</div>
);
}
Inside the render function we use the conditional operator (?) to return the button component if this.state.isMouseInside is truthy.
There is another approach that uses a reusable render component that would make components 'hoverable' or 'revealable' - whatever makes sense.
class Hoverable extends Component {
constructor() {
super();
this.state = {
isMouseInside: false
};
}
mouseEnter = () => {
this.setState({ isMouseInside: true });
}
mouseLeave = () => {
this.setState({ isMouseInside: false });
}
render() {
return this.props.children(
this.state.isMouseInside,
this.mouseEnter,
this.mouseLeave
)
}
}
Then create the functional component that represents the hoverable element. E.g an album
const HoverableElement = props => (
<Hoverable>
{(isMouseInside, mouseEnter, mouseLeave) => (
<div className="menu-item">
<div onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
<h2>{props.title}</h2>
</div>
{isMouseInside && props.children}
</div>
)}
</Hoverable>
)
Finally, use the HoverableElement to render a list of elements that will each be 'hoverable' e.g an array of albums
class HoverableElementsList extends Component {
render() {
return (
<div>
<HoverableElement title="First Menu">
<p>Some children content</p>
</HoverableElement>
</div>
)
}
}

Categories

Resources