React Star Widget - why do all stars update on single click? - reactjs

I am trying to create a star widget. I have a state array for each star, but when I click one of the stars, ALL of the stars set themselves to that state. I am very lost on this, please halp. I have added a lot of debugging logs. The moment I set newStars[i] = currentStar;, the entire newStars array gets updated, but I'm failing to see why.
Also, here is the code pen link: https://codepen.io/trismi/pen/zYZpvQq?editors=1111
HTML:
<div id="root">
</div>
CSS (plus the awesome fonts stylesheet linked in the codepen)
.star {
display: inline-block;
width: 30px;
text-align: center;
color: #ddd;
font-size: 20px;
transform: scale(.8);
transition: transform 50ms ease;
&:hover,
&.semi-active {
color: gold;
transform: scale(1);
}
&.selected {
color: orange;
transform: scale(1);
}
}
JAVASCRIPT
function Star(props) {
console.log(props);
console.log(props.index);
let classes = 'star' + (props.selected ? ' selected' : '') + (props.hover ? ' semi-active' : '');
return (
<div className={classes} onClick={props.onClick}>
<i className="fas fa-star"></i>
</div>
);
}
class RatingWidget extends React.Component {
constructor(props){
super(props);
this.state = {
stars: Array(5).fill({
selected: false,
hover: false,
}),
}
}
handleClick(currentStar, index) {
console.log('\n\n\n******CLICK');
console.log("star state on click", currentStar);
console.log("index", index);
let newStars = this.state.stars.slice();
let newStar = newStars[index];
console.log("new star ", newStar);
newStar.selected = !newStar.selected;
newStars[index] = newStar;
console.log("stars", newStars);
this.setState({
stars: newStars
});
}
render() {
let stars = this.state.stars.map((rating, index) => {
return (
<Star
key={index}
index={index}
onClick={() => this.handleClick(rating, index)}
selected={rating.selected}
hover={rating.hover}
/>);
});
return (
<div className="RatingWidget">
Future rating widget
{stars}
</div>
);
}
}
ReactDOM.render(<RatingWidget />, document.getElementById('root'));

The problem is here:
Array(5).fill({
selected: false,
hover: false,
})
you are filling the same object (same reference) to each element of the array.
Try using:
Array(5).fill(null).map(() => ({
selected: false,
hover: false,
}))
Or use Array.from():
Array.from({length: 5}, () => ({ selected: false, hover: false}))

You can have the below handleClick function
I updated let newStar = newStars[index]; to let newStar = {...newStars[index]};
handleClick(currentStar, index) {
console.log('\n\n\n******CLICK');
console.log("star state on click", currentStar);
console.log("index", index);
let newStars = this.state.stars.slice();
let newStar = {...newStars[index]};
console.log("new star ", newStar);
newStar.selected = !newStar.selected;
newStars[index] = newStar;
console.log("stars", newStars);
this.setState({
stars: newStars
});
}

Related

React Select with avatar class component whats wrong here select not populating

Hi I had working select box with avatar on functional aporx. But I need build in functions to change selectbox properties like hidden: true/false, update option data function to add new option data to display selectbox with certain values on fly.
What I wrong here? Render part works as tested with functional version. The class factoring misses something as select box does not get info ether options and avatar to display and no width calculation happening.
Orginal functional code: https://codesandbox.io/s/react-select-longest-option-width-geunu?file=/src/App.js
Class based works but nodatin selectbox. Here is app.js with select.js fiddle: https://codesandbox.io/s/react-select-longest-option-width-forked-plqq0p?file=/src/Select.js
Source:
import React, { useRef } from "react";
import Select from "react-select";
class RtSelect extends React.Component {
constructor(props) {
super();
this.state = {
info: props.info,
options: props.options,
hidden: props.hidden,
menuIsOpen: false,
menuWidth: "",
IsCalculatingWidth: false
};
this.selectRef = React.createRef();
this.onMenuOpen = this.onMenuOpen.bind(this);
}
componentDidMount() {
if (!this.menuWidth && !this.isCalculatingWidth) {
setTimeout(() => {
this.setState({IsCalculatingWidth: true});
// setIsOpen doesn't trigger onOpenMenu, so calling internal method
this.selectRef.current.select.openMenu();
this.setState({MenuIsOpen: true});
}, 1);
}
}
onMenuOpen() {
if (!this.menuWidth && this.isCalculatingWidth) {
setTimeout(() => {
const width = this.selectRef.current.select.menuListRef.getBoundingClientRect()
.width;
this.setState({menuWidth: width});
this.setState({IsCalculatingWidth: false});
// setting isMenuOpen to undefined and closing menu
this.selectRef.current.select.onMenuClose();
this.setState({MenuIsOpen: true});
}, 1);
}
}
styles = {
menu: (css) => ({
...css,
width: "auto",
...(this.isCalculatingWidth && { height: 0, visibility: "hidden" })
}),
control: (css) => ({ ...css, display: "inline-flex " }),
valueContainer: (css) => ({
...css,
...(this.menuWidth && { width: this.menuWidth })
})
};
setData (props) {
this.setState({
info: props.info,
options: props.options,
hidden: props.hidden
})
}
render() {
return (
<div style={{ display: "flex" }}>
<div style={{ margin: "8px" }}>{this.info}</div>
<div>
<Select
ref={this.selectRef}
onMenuOpen={this.onMenuOpen}
options={this.options}
menuIsOpen={this.menuIsOpen}
styles={this.styles}
isDisabled={this.hidden}
formatOptionLabel={(options) => (
<div className="select-option" style={{ display: "flex" }}>
<div style={{ display: "inline", verticalAlign: "center" }}>
<img src={options.avatar} width="30px" alt="Avatar" />
</div>
<div style={{ display: "inline", marginLeft: "10px" }}>
<span>{options.label}</span>
</div>
</div>
)}
/>
</div>
</div>
);
}
}
export default RtSelect;
Got it working!
I had removed "undefined" from onOpen setState function. I compared those 2 fiddles and finally got it working.
// setting isMenuOpen to undefined and closing menu
this.selectRef.current.select.onMenuClose();
this.setState({MenuIsOpen: undefined});

Google Map info window not loading - React

I'm very new to React, but I've managed to build a functioning Google Map component that is successfully centering in a location derived from a search and populating the map with results from Json results. However, I can't get an Info Window to show when clicking one of the map markers and I don't know where I'm going wrong.
Here's the code for the Map Marker and the Info Window:
const ReactMapComponent = ({ text }) => (<div style={{ background: 'url(/olb/images/mapMarker.png)', height: '44px', width: '35px', cursor: 'pointer' }}>{}</div>);
const InfoWindow = ({ text }) => (<div style={{ background: '#fff', height: '100px', width: '135px', cursor: 'pointer', position: 'relative', zIndex: 100 }}>{}</div>);
class VenueMapChild extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: false
}
}
handleToggleOpen = () => {
this.setState({
isOpen: true
});
};
handleToggleClose = () => {
this.setState({
isOpen: false,
});
};
render() {
return (
<ReactMapComponent
key={this.props.key}
lat={this.props.lat}
lng={this.props.lng}
text={this.props.hotelName}
onClick={() => this.handleToggleOpen()}
>
{this.state.isOpen &&
<InfoWindow onCloseClick={() => this.handleToggleClose()}>
<span>Something</span>
</InfoWindow>
}
</ReactMapComponent>
);
}
}
Thanks in advance

How to handle state on Flat List Items in React-Native

I am setting a counter on the items of the FlatList component of React-Native. How can I update the list item every time the user press the "+" or "-" button?
I am currently being able to update the value on the state, however the list doesnt display the new state. I have tried adding extraData component to the FlatList, but it doesnt seem to update anyways.
This is the data structure
data: [
{
id: 1,
name: "Bread",
price: "400",
imageS: "../resources/pan-corteza-blanda.jpg",
quantity: 2
},
... more data
This is the function that handles the increment
handleIncrement = i => {
this.setState(state => {
const formatData = state.data.map((item, j) => {
console.log("Id", i + " /// " + item.id);
if (item.id === i) {
item.quantity = item.quantity + 1;
return item;
} else {
return item;
}
});
console.log("FormatData" + formatData); //Displays the correct quantity of the item updated
return {
formatData
};
});
};
And this is the list component
<FlatList
data={this.state.data}
style={styles.list}
extraData={this.state.data}
renderItem={this.renderItem}
/>
I expect to update the text component of the list item with the right quantity value every time a user presses the "+" or "-" button.
You need to update the data state instead of returning an item.
handleIncrement = i => {
const item = this.state.data[i];
this.setState({
data: [
...this.state.data.slice(0, i),
Object.assign({}, this.state.data[i], { quantity: item.quantity + 1 }),
...this.state.data.slice(i + 1)
]
});
};
You can refactor the function and use it for both - and +.
// pass array index and quantity 1 for + and -1 for -
handleIncrement = (i, qty) => {
const item = this.state.data[i];
if (item && item.quantity === 0 && qty === -1) {
return;
}
this.setState({
data: [
...this.state.data.slice(0, i),
Object.assign({}, this.state.data[i], { quantity: item.quantity + qty, }),
...this.state.data.slice(i + 1),
],
});
};
Below is demo that uses the above function, it's in reactjs. The function will work in react native too.
h1, p {
font-family: Lato;
}
.container {
display: flex;
flex-direction: row;
border-bottom-style: solid;
margin-bottom: 5px;
}
.image-container {
flex-grow: 0;
}
.info-container {
display: flex;
margin-left: 10px;
flex-direction: row;
}
.title {
margin-top: 0;
}
.titleContainer {
width: 100px;
}
.cover {
width: 30px;
}
.buttons {
flex-grow: 1;
display: flex;
margin-left: 10px;
}
.incrementButtons {
width: 20px;
height: 20px;
margin-left: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
class Item extends React.Component {
render() {
return (
<div className="container">
<div className="image-container">
<img className="cover" src={this.props.image} />
</div>
<div className="info-container">
<div className="titleContainer">
<p className="title">{this.props.title}</p>
</div>
<div className="buttons">
<p className="title">{this.props.qty}</p>
<img onClick={() => this.props.increment(this.props.index, -1)} className="incrementButtons" src="https://img.icons8.com/metro/26/000000/minus-math.png" />
<img onClick={() => this.props.increment(this.props.index, 1)} className="incrementButtons" src="https://img.icons8.com/metro/26/000000/plus-math.png" />
</div>
</div>
</div>
)
}
}
class App extends React.Component {
state = {
data: [
{
id: 1,
name: 'Avocado',
price: '400',
imageS: 'https://img.icons8.com/metro/26/000000/avocado.png',
quantity: 0,
},
{
id: 6,
name: 'Bread',
price: '300',
imageS: 'https://img.icons8.com/metro/26/000000/bread.png',
quantity: 0,
},
{
id: 2,
name: 'Milk',
price: '300',
imageS: 'https://img.icons8.com/metro/26/000000/milk-bottle.png',
quantity: 0,
},
],
};
handleIncrement = (i, qty) => {
const item = this.state.data[i];
if (item && item.quantity === 0 && qty === -1) {
return;
}
this.setState({
data: [
...this.state.data.slice(0, i),
Object.assign({}, this.state.data[i], { quantity: item.quantity + qty, }),
...this.state.data.slice(i + 1),
],
});
};
render() {
const items = this.state.data.map((item, index) => (
<Item qty={item.quantity} index={index} key={index} increment={this.handleIncrement} title={item.name} image={item.imageS} />
))
return (
<div>
{items}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
this.state.data is never changed on your handleIncrement, and this is what you are passing into FlatList. Thats why FlatList doesn't update.
After handleIncrement runs, the only thing that changes in your state is:
{
formatData: [...stuff here]
}
Maybe you want to pass down this.state.formatData instead or rename it in handleIncrement to data.
Also, your state structure would probably be better as a map, where the keys are itemIds. This way you don need to map through the whole list every time you want to increment the quantity.
e.g.
{
1: {
id: 1,
name: "Bread",
price: "400",
imageS: "../resources/pan-corteza-blanda.jpg",
quantity: 2
},
// more data...
}
Now, your handleIncrement looks like this:
handleIncrement = itemId => this.setState(prevState => ({
...prevState,
[itemId]: ++prevState[itemId].quantity
}))

Toggle Button Group in React

New to react and trying to use in a new electron learning project I have. I'm trying to build a really basic drawing app.
I created a ToolbarButton class. That would represent each button and a Toolbar that would manage the button group. Example If you pick a 'primary' tool then it should turn off all other primary tools and leave only your current selection.
In jQuery I'd just do something like
let button = $(this);
let toolbar = $(this).parent();
toolbar.find('button.toolbar-button').removeClass('active');
button.addClass('active');
How would I do the same in react? I can toggle what I want to with setState from within the ToggleButton but separating it out into a prop seems to be an answer, but then I need to have the Toolbar manage the button 'active' states and I'm not sure how to do that. Think the answer is in ReactDOM, super newbie to react so apologize if the answer is overly obvious.
import React from 'react';
import FontAwesome from 'react-fontawesome';
import { ButtonGroup } from 'react-bootstrap';
import { ChromePicker} from 'react-color';
class ToolbarButton extends React.Component
{
state =
{
active: false
}
handleClick = ()=> {
if(this.props.onClick)
{
this.props.onClick();
}
this.setState({ active: !this.state.active});
}
render(){
return <div className={`btn btn-primary${this.state.active ? ' active' : ''}`} onClick={this.handleClick}>{this.props.children}</div>
}
}
class ColorPickerButton extends React.Component
{
constructor(props)
{
super(props);
this.state = {
displayColorPicker: false,
color: { r: 255, g: 255, b: 255, a:1 }
}
}
state = {
flyout: 'bottom',
displayColorPicker: false,
color: { r: 255, g: 255, b: 255, a:1 }
}
/* This button should have an option to display how the colorpicker flys out */
static flyoutStyles =
{
normal: { },
top: {top: '0px' },
bottom: { top: '100%' },
left: { right: '100%' },
right: { left: '100%' }
}
handleClick = (e) => {
this.setState({ displayColorPicker: !this.state.displayColorPicker});
}
handleClose = () => {
this.setState({ displayColorPicker: false });
}
handleChange = (color) => {
this.setState({ color: color.rgb });
}
stopPropagation = (e) => {
e.stopPropagation();
}
render()
{
const swatchStyle = {
backgroundColor: `rgba(${this.state.color.r},
${this.state.color.g},
${this.state.color.b},
${this.state.color.a})`,
height: '16px',
width: '16px',
border: '1px solid white'
};
const popup = {
position: 'absolute',
zIndex: 2,
top: 'calc(100% + 2px)'
};
const cover = {
position: 'fixed',
top: '0px',
right: '0px',
left: '0px',
bottom: '0px',
};
return (
<ToolbarButton onClick={this.handleClick} active={this.state.displayColorPicker}>
<div style={swatchStyle} />
{
this.state.displayColorPicker ?
<div style={popup} onClick={this.stopPropagation}>
<div style={ cover } onClick={ this.handleClose }/>
<ChromePicker color={this.state.color} onChange={this.handleChange} />
</div>
: null
}
</ToolbarButton>
);
}
}
export class CanvasToolbar extends React.Component
{
handleClick = (e) => {
}
render(){
return (<div className="CanvasToolbar">
<ButtonGroup vertical>
<ToolbarButton>
<FontAwesome name='paint-brush' />
</ToolbarButton>
<ToolbarButton>
<FontAwesome name='eraser' />
</ToolbarButton>
<ToolbarButton>
<FontAwesome name='magic' />
</ToolbarButton>
<ColorPickerButton />
</ButtonGroup>
</div>);
}
}

Show or Hide a particular element in react

I have to show list of faqs and I need to hide the answers of the questions. When I click on the question, the answer for that particular question need to be shown. My problem is, I have a bunch of questions and when i click the button, it will show all of the answer instead of the specific answer to that question.
class Faqs extends Component {
constructor(props){
super(props);
this.state = {
isHidden: true
}
}
toggleHidden () {
this.setState({
isHidden: !this.state.isHidden
})
}
render() {
return (
<div>
<span onClick={() => this.toggleHidden()}><strong>This is the question</strong></span>
{!this.state.isHidden && <p>Answer for the question</p>} <br/>
<span onClick={() => this.toggleHidden()}><strong>Question2</strong></span>
{!this.state.isHidden && <p>Answer2</p>} <br/>
<hr></hr>
</div >
)
}
}
You can break your component to one more level to have a sub component which renders only the question and corresponding answer. Pass the question and answers as props. In that way you can use the same component for all questions and yet every question/answer pair will have their own state.
class Faq extends Component{
state = {isHidden: true}
toggleHidden = ()=>this.setState((prevState)=>({isHidden: !prevState.isHidden}))
render(){
return(
<div>
<span onClick={this.toggleHidden}>
<strong>{props.question}</strong></span>
{!this.state.isHidden && <p>{props.answer}</p>}
</div>
)
}
}
class Faqs extends Component {
render() {
return (
<div>
<Faq question={"Question 1"} answer={"answer 1"} />
<Faq question={"Question 2"} answer={"answer 2"} />
</div >
)
}
}
Ideally you would list FAQs in some kind of list - then as you iterate over them, each will have an index number assigned to it - then as you toggle individual answers, you store that index in the state and operate on DOM via that number.
edit. In current day and age, it's only appropriate to show example using hooks:
const {useState} = React;
const FaqApp = () => {
const [ selectedQuestion, toggleQuestion ] = useState(-1);
function openQuestion(index) {
toggleQuestion(selectedQuestion === index ? -1 : index);
}
const faqs = getFaqs();
return (
<div>
<h2>FAQs:</h2>
{faqs.map(( { question, answer}, index) => (
<div key={`item-${index}`} className={`item ${selectedQuestion === index ? 'open' : ''}`}>
<p className='question' onClick={() => openQuestion(index)}>{question}</p>
<p className='answer'>{answer}</p>
</div>
))}
</div>
)
}
function getFaqs() {
const faqs = [
{
question: 'Question 1',
answer: 'answer 1'
},
{
question: 'Question 2',
answer: 'answer 2'
}
];
return faqs;
}
ReactDOM.render(
<FaqApp />,
document.getElementById("react")
);
body {
background: #fff;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
h2 {
margin-bottom: 11px;
}
.item + .item {
margin-top: 11px;
}
.question {
font-weight: bold;
cursor: pointer;
}
.answer {
display: none;
}
.open .answer {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
older version of this post:
I've written a quick example that allows you to have multiple questions:
class FaqApp extends React.Component {
constructor(props) {
super(props)
this.state = {
// start the page with all questions closed
selectedQuestion: -1
};
this.openQuestion = this.openQuestion.bind(this);
}
getFaqs() {
// some service returning a list of FAQs
const faqs = [
{
question: 'Question 1',
answer: 'answer 1'
},
{
question: 'Question 2',
answer: 'answer 2'
}
];
return faqs;
}
openQuestion(index) {
// when a question is opened, compare what was clicked and if we got a match, change state to show the desired question.
this.setState({
selectedQuestion: (this.state.selectedQuestion === index ? -1 : index)
});
}
render() {
// get a list of FAQs
const faqs = this.getFaqs();
return (
<div>
<h2>FAQs:</h2>
{faqs.length && faqs.map((item, index) => (
<div key={`item-${index}`} className={`item ${this.state.selectedQuestion === index ? 'open' : ''}`}>
<p className='question' onClick={() => this.openQuestion(index)}>
{item.question}
</p>
<p className='answer'>
{item.answer}
</p>
</div>
))}
</div>
)
}
}
ReactDOM.render(<FaqApp />, document.querySelector("#app"))
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
h2 {
margin-bottom: 11px;
}
.item + .item {
margin-top: 11px;
}
.question {
font-weight: bold;
cursor: pointer;
}
.answer {
display: none;
}
.open .answer {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
The issue is that you're using one boolean piece of state to control the logic for multiple pieces. This is a classic scenario to use separate components.
Create a new component ToggleQuestion that encapsulates the mechanic of show/reveal.
The Faqs component instead manages a list of ToggleQuestion components.
const QUESTIONS = [
{ title: 'q1', answer: 'a1' },
{ title: 'q2', answer: 'a2' }
]
class ToggleQuestion extends React.Component {
constructor (props) {
super(props)
this.state = { isHidden: true }
}
toggleHidden () {
this.setState({ isHidden: !this.state.isHidden })
}
render () {
const { question, answer } = this.props
const { isHidden } = this.state
return (
<div>
<span>{question}</span>
{ !isHidden && <span>{answer}</span> }
<button onClick={this.toggleHidden.bind(this)}>
Reveal Answer
</button>
</div>
)
}
}
class Faqs extends React.Component {
render () {
return (
<div>
{ QUESTIONS.map(question => (
<ToggleQuestion
question={question.title}
answer={question.answer}
/>
))}
</div>
)
}
}
ReactDOM.render(<Faqs />, document.getElementById('container'))
<div id='container'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
I would write a different handler for the answer. In the future if you need more logic for the answer will be scalable. Notice renderAnswer
class Faqs extends Component {
constructor(props){
super(props);
this.state = {
isHidden: true
}
}
toggleHidden () {
this.setState({
isHidden: !this.state.isHidden
})
}
renderAnswer() {
if (this.state.isHidden) {
return;
}
return (
<p>Answer</p>
);
}
render() {
return (
<div>
<span onClick={() => this.toggleHidden()}><strong>This is the question</strong></span>
{ this.renderAnswer() } <br/>
<span onClick={() => this.toggleHidden()}><strong>Question2</strong></span>
{ this.renderAnswer() } <br/>
<hr></hr>
</div >
)
}
}
This is another way to do what you want. (This one will only make it possible to have one open at a time)
class Faqs extends Component {
constructor(props){
super(props);
this.state = {
hiddenId: null,
}
}
setHiddenId(id) {
this.setState({
hiddenId: id
})
}
render() {
return (
<div>
<span onClick={() => this.setHiddenId('one')}><strong>This is the question</strong></span>
{this.state.hiddenId === 'one' && <p>Answer for the question</p>} <br/>
<span onClick={() => this.setHiddenId('two')}><strong>Question2</strong></span>
{this.state.hiddenId === 'two' && <p>Answer2</p>} <br/>
<hr></hr>
</div >
)
}
}

Resources