I have a function which creates 2 divs when changing the number of items correspondingly (say if we choose 5 TVs we will get 5 divs for choosing options). They serve to make a choice - only one of two possible options should be chosen for every TV, so when we click on one of them, it should change its border and background color.
Now I want to create a dynamic stylization for these divs: when we click on them, they should get a new class (tv-option-active) to change their styles.
For that purposes I used 2 arrays (classesLess and classesOver), and every time we click on one of divs we should remove a class if it's already applied to the opposite option and push the class to the target element - thus only one of options will have tv-option-active class.
But when I click on a div I do not get anything - when I open the document in the browser and inspect the elements, the elements do not even receive new class on click - however, when I console log the classes variable that should apply to an element, it is the way it should be - "less tv-option-active" or "over tv-option-active". I applied join method to remove the comma.
I checked the name of imported css file and it is ok so the problem is not there, also I applied some rules just to make sure the problem is not there and it worked when it's not dynamic (I mean no clicks are needed).
So my list of reasons causing that trouble seems to be not working.
I also tried to reorganize the code in order to not call a function in render return - putting mapping directly to render return, but this also didn't work.
Can anyone give me a hint why it is that?
Here is my code.
import React from 'react'
import { NavLink, withRouter } from 'react-router-dom'
import './TVMonting.css'
import PageTitle from '../../PageTitle/PageTitle'
class TVMontingRender extends React.Component {
state = {
tvData: {
tvs: 1,
under: 0,
over: 0,
},
}
render() {
let classesLess = ['less']
let classesOver = ['over']
const tvHandlers = {
tvs: {
decrease: () => {
if (this.state.tvData.tvs > 1) {
let tvData = {
...this.state.tvData,
}
tvData.tvs -= 1
this.setState({ tvData })
}
},
increase: () => {
if (this.state.tvData.tvs < 5) {
let tvData = {
...this.state.tvData,
}
tvData.tvs += 1
this.setState({ tvData })
}
},
},
}
const createDivs = () => {
const divsNumber = this.state.tvData.tvs
let divsArray = []
for (let i = 0; i < divsNumber; i++) {
divsArray.push(i)
}
return divsArray.map((i) => {
return (
<React.Fragment key={i}>
<div
className={classesLess.join(
' '
)}
onClick={() => {
const idx = classesOver.indexOf(
'tv-option-active'
)
if (idx !== -1) {
classesLess.splice(
idx,
1
)
}
classesLess.push(
'tv-option-active'
)
}}
>
Under 65
</div>
<div
className={classesOver.join(
' '
)}
onClick={() => {
const idx = classesLess.indexOf(
'tv-option-active'
)
if (idx !== -1) {
classesOver.splice(
idx,
1
)
}
classesOver.push(
'tv-option-active'
)
// classesOver.join(' ')
}}
>
Over 65
</div>
</React.Fragment>
)
})
}
return (
<div>
<button onClick={tvHandlers.tvs.decrease}>
-
</button>
{this.state.tvData.tvs === 1 ? (
<h1> {this.state.tvData.tvs} TV </h1>
) : (
<h1> {this.state.tvData.tvs} TVs </h1>
)}
<button onClick={tvHandlers.tvs.increase}>
+
</button>
{createDivs()}
</div>
)
}
}
export default withRouter(TVMontingRender)
CSS file is very simple - it just adds a border.
P.S. I know that with this architecture when I click on one of the divs all the divs will get tv-option-active class, but for now I just want to make sure that this architecture works - since I'm relatively new in React 🙂Thanks in advance!
Components won't have their lifecycle triggered if you are mutating a variable. You need a state for that purpose, which stores the handled data.
In your case you need some state to say which div has the active class, under or over. You can also abstract each rendered Tv to another Class component. This way you achieve independent elements that control their own class, rather than changing all others.
For that I created a Tv class, where I simplified some of the logic:
class Tv extends React.Component {
state = {
activeGroup: null
}
// this will update which group is active
changeActiveGroup = (activeGroup) => this.setState({activeGroup})
// activeClass will return 'tv-option-active' if that group is active
activeClass = (group) => (group === this.state.activeGroup ? 'tv-option-active' : '')
render () {
return (
<React.Fragment>
<div
className={`less ${ activeClass('under') }`}
onClick={() => changeActiveGroup('under')}
>
Under 65
</div>
<div
className={`over ${ activeClass('over') }`}
onClick={() => changeActiveGroup('over')}
>
Over 65
</div>
</React.Fragment>
)
}
}
Your TvMontingRender will be cleaner, also it's better to declare your methods at your class body rather than inside of render function:
class TVMontingRender extends React.Component {
state = {
tvData: {
tvs: 1,
under: 0,
over: 0,
}
}
decreaseTvs = () => {
if (this.state.tvData.tvs > 1) {
let tvData = {
...this.state.tvData,
}
tvData.tvs -= 1
this.setState({ tvData })
}
}
increaseTvs = () => {
if (this.state.tvData.tvs < 5) {
let tvData = {
...this.state.tvData,
}
tvData.tvs += 1
this.setState({ tvData })
}
}
createDivs = () => {
const divsNumber = this.state.tvData.tvs
let divsArray = []
for (let i = 0; i < divsNumber; i++) {
divsArray.push(i)
}
// it would be better that key would have an unique generated id (you could use uuid lib for that)
return divsArray.map((i) => <Tv key={i} />)
}
render() {
return (
<div>
<button onClick={this.decreaseTvs}>
-
</button>
{this.state.tvData.tvs === 1 ? (
<h1> {this.state.tvData.tvs} TV </h1>
) : (
<h1> {this.state.tvData.tvs} TVs </h1>
)}
<button onClick={this.increaseTvs}>
+
</button>
{this.createDivs()}
</div>
)
}
}
Note: I didn't change the key you are passing to Tv, but when handling an array that you manipulate somehow it's often better to pass an unique id identifier. There are some libs for that like uuid, nanoID.
When handling complex class logic, you may consider using libs like classnames, that would make your life easier.
Related
I'm having a problem with inline style changes that are reset when my dispatch is finished, because the state is being re-rendered, despite the other functionality of my component is working (you can still see that the counter is not stopping).
Here's a demonstration of what I mean.
You can see that the orange bar of the left box vanishes when the orange bar of the right bar finishes (the animation ends). Essentially what I'm doing here is changing the width property in inline styles.
import React, { useEffect, useRef } from "react";
import { useDispatch, connect } from "react-redux";
import { addProfessionExperience } from "../../actions/index";
import "./Professions.sass";
const timers = [];
const progressWidths = [];
const mapStateToProps = (state, ownProps) => {
const filterID = +ownProps.match.params.filter;
const { professions, professionExperience } = state;
return {
professions: professions.find(item => item.id === filterID),
professionExperience: professionExperience
};
};
const produceResource = (dispatch, profession, sub, subRef) => {
if(timers[sub.id]) return;
/*
* Begin the progress bar animation/width-change.
*/
Object.assign(subRef.current[sub.id].style, {
width: "100%",
transitionDuration: `${sub.duration}s`
});
/*
* Updates the progress text with the remaining time left until done.
*/
let timeLeft = sub.duration;
const timeLeftCountdown = _ => {
timeLeft--;
timeLeft > 0 ? setTimeout(timeLeftCountdown, 1000) : timeLeft = sub.duration;
subRef.current[sub.id].parentElement.setAttribute("data-duration", timeLeft + "s");
}
setTimeout(timeLeftCountdown, 1000);
/*
* Dispatch the added experience from profession ID and sub-profession level.
* We do not allow duplicate timers, only one can be run at a time.
*/
const timer = setTimeout(() => {
Object.assign(subRef.current[sub.id].style, {
width: "0%",
transitionDuration: "0.2s"
});
dispatch(addProfessionExperience({ id: profession.id, level: sub.level }));
delete timers[sub.id];
}, sub.duration * 1000);
timers[sub.id] = timer;
};
const isSubUnlocked = (professionMaxExperience, subLevel, professionExperience) => {
if(professionExperience <= 0 && subLevel > 1) return false;
return professionExperience >= getExperienceThreshold(professionMaxExperience, subLevel);
};
const getExperienceThreshold = (professionMaxExperience, subLevel) => (((subLevel - 1) * 1) * (professionMaxExperience / 10) * subLevel);
const ConnectedList = ({ professions, professionExperience }) => {
const currentExperience = professionExperience.find(item => item.profession === professions.id);
const subRef = useRef([]);
const dispatch = useDispatch();
useEffect(() => {
subRef.current = subRef.current.slice(0, professions.subProfessions.length);
}, [professions.subProfessions]);
return (
<div>
<div className="list">
<ul>
{professions.subProfessions.map(el => {
const unlocked = isSubUnlocked(
professions.maxExperience,
el.level,
(currentExperience ? currentExperience.amount : 0)
);
const remainingExperience = getExperienceThreshold(professions.maxExperience, el.level) - (currentExperience ? currentExperience.amount : 0);
return (
<li
key={Math.random()}
style={{ "opacity": unlocked ? "1" : "0.5" }}
>
<div className="sprite">
<img alt="" src={`/images/professions/${el.image}.png`} />
</div>
<div className="caption">{el.name}</div>
<div
className="progress-bar"
data-duration={unlocked ? `${el.duration}s` : `${remainingExperience} XP to Unlock`}
data-identifier={el.id}
>
<span ref={r => subRef.current[el.id] = r} ></span>
</div>
<div className="footer">
<button
className="btn"
onClick={() => unlocked ? produceResource(dispatch, professions, el, subRef) : false}
>
{unlocked ?
`Click` :
<i className="fa fa-lock"></i>
}
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};
const List = connect(mapStateToProps)(ConnectedList);
export default List;
How can I make it so the orange bars persist on their own and not disappears when another one finishes?
One problem is that you're using Math.random() to generate your keys. Keys are what the virtual DOM uses to determine whether an element is the "same" as the one on a previous render. By using a random key, you're telling the virtual DOM that you want to spit out a brand new DOM element instead of reusing the prior one, which means the new one won't retain any of the side effects you placed on the original element. Read up on React's reconciliation for more info on this.
Try to use keys that logically represent the thing you're rendering. In the case of your code, el.id looks like it may be a unique identifier for the subprofession you're rendering. Use that for the key instead of Math.random().
Additionally, refs are going to make reasoning about your code really difficult. Rather than using refs to manipulate your DOM, use state manipulation and prop passing, and let React re-render your elements with the new attributes.
I'm new here and I have a question that I couldn't find answer to..
I am currently working on a website using ReactJS, and I want to have a button that fills itself whenever the user clicks on it. The button should have a total of 5 stages to it.
I am not asking for you to code it for me, but simply help me finding the best approach to this thing.
So what exactly am I asking? As you can see in this
It's a boxed element that whenever the user clicks on it (it can click on the whole element), the progress fills and it becomes something like this
So the first line is now marked. When the user presses on it again, the 2nd bar fills
Important - there will be text inside these bars that fills.
What have I done so far? I have been thinking of having 5 different images for every time the user presses on the element, but I was wondering if there might be a better approach to it (Like having the DIV background the image, and have sub-divs that fills up whenever the user presses... )
I hope I made myself clear, and thank you all for your time!
Here is a working example. You don't need an image for all of the different states. It is far more flexible to do this dynamically with HTML.
The key to this is keeping track of the number of times the button has been clicked. In this example it uses currentState to keep track of how many times it has been clicked.
const defaultStyle = {
width: 100,
padding: 5,
};
class MultiLevelButton extends React.Component {
constructor(props) {
super(props);
this.state = {
currentState: 0,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const { currentState } = this.state;
if (currentState < this.props.states.length) {
this.setState({
currentState: currentState + 1,
});
}
}
reset() {
this.setState({
currentState: 0,
});
}
render() {
const { currentState } = this.state;
const { states } = this.props;
return (
<button onClick={this.handleClick} style={{border: 'none', outline: 'none'}}>
{
states
.map((s, i) => {
const stateNumber = states.length - i;
const overrides = stateNumber <= currentState ? { backgroundColor: '#000', color: '#fff' } : {};
const style = {
...defaultStyle,
backgroundColor: s,
...overrides,
};
return <div style={{...style}}>{stateNumber}</div>
})
}
</button>
)
}
}
const buttonRef = React.createRef();
ReactDOM.render(
<div>
<MultiLevelButton ref={buttonRef} states={['#bbb', '#ccc', '#ddd', '#eee', '#fff']} />
<MultiLevelButton states={['#fcc', '#cfc', '#ccf']} />
<div>
<button onClick={() => buttonRef.current.reset()}>Reset</button>
</div>
</div>,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app" />
As you are using react. use create step/level as component and you will pass props of styles. you can map that component 5 times or n times depending upon requirement. The view you have shown no need images use css to achieve it.
change props of component when user click on it.
You can use state in order to keep the click count and change the button background on the basis of click count.
const colors = ["#dfddc7", "#f58b54", "#a34a28", "#211717"]; //color array
class App extends Component {
constructor(props) {
super(props);
this.state = {
index : 0
}
}
handleChange() { // function will be called whenever we click on button
let {index} = this.state;
if (index >= 5) {
return; // you don't want to change color after count 5
}
index++;
console.log(index);
this.setState({index})
}
render() {
const {index} = this.state;
return (
<div className="App">
<button style = {{background:colors[index]}} //we are setting dynamic color from array on the basis of index
onClick = {this.handleChange.bind(this)}> click to change images
</button>
</div>
);
}
}
You can place <div></div> inside a button with different background color instead of images.
In the following example, I hold the number of clicks in the state. By comparing this value with the index of the step, you can see if it needs to be green or transparent
const numberOfSteps = 5;
class App extends React.Component {
state = {
numberOfClicks: 0
};
handleClick = e => {
this.setState({
numberOfClicks: (this.state.numberOfClicks + 1) % (numberOfSteps + 1)
}); // Use module (%) to reset the counter after 5 clicks
};
render() {
const { numberOfClicks } = this.state;
const steps = Array(numberOfSteps)
.fill()
.map((v, i) => i)
.map((i, index) => {
const color = numberOfClicks >= index + 1 ? "limegreen" : "transparent";
return (
<div
style={{
backgroundColor: color,
height: "30px",
border: ".5px solid gray"
}}
>
Bar {index + 1}
</div>
);
});
return (
<button
className="button"
style={{ height: "200px", width: "200px", backgroundColor: "lightgreen" }}
onClick={this.handleClick}
>
{steps}
</button>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root">
<!-- This element's contents will be replaced with your component. -->
</div>
The key concept is state. When you click your button you should set some state, and when you render the button you should render it based on the state.
Here's a simple example where I render a button which contains 5 div elements that are filled (by setting backgroundColor style property) based on the state.
class Example extends Component {
constructor(props) {
super(props);
this.state = {
buttonState: 0,
};
}
onClick = () => {
let buttonState = this.state.buttonState;
buttonState++;
if(buttonState > 4) buttonState = 0; // wrap around from 4 - 0
this.setState({buttonState: buttonState});
}
// render a button element with some text, and a background color based on whether filled is true/false
renderButtonElement(elementText, filled) {
const backgroundStyle = filled ? {backgroundColor: 'green'} : {backgroundColor: 'white'};
const textStyle = {color: 'grey'};
return(
<div style={[backgroundStyle, textStyle]}>
<div>{elementText}</div>
</div>
);
}
render() {
return(
<button
onClick={this.onClick}
>
{/* make a temporary array of 5 elements, map over them and render an element for each one */}
{Array(5).fill().map((_, i) => this.renderButtonElement(i + 1, i <= this.state.buttonState))}
</button>
);
}
}
I'm having an issue re-rendering the DOM after something has been spliced from an array in my state variable. Here is the code for this component:
class AddMusicResource extends Component {
removeFile(file, event) {
var filesPreview = this.state.filesPreview
var filesToBeSent = this.state.filesToBeSent
var i = filesToBeSent.indexOf(file)
console.log(i)
if(i !== -1) {
filesToBeSent.splice(i, 1)
filesPreview.splice(i, 1)
}
this.setState({filesToBeSent, filesPreview});
this.setState(this.state)
}
onDrop(acceptedFiles, rejectedFiles) {
var filesToBeSent=this.state.filesToBeSent;
for(var file in acceptedFiles) {
console.log('Accepted files: ', acceptedFiles[file].name);
filesToBeSent.push(acceptedFiles[file]);
}
var filesPreview=[];
for(var i in filesToBeSent){
filesPreview.push(
<div key={i}>
{filesToBeSent[i].name}
<IconButton
iconClassName="material-icons"
onClick={this.removeFile.bind(this, filesToBeSent[i])}
>
close
</IconButton>
</div>
)
}
this.setState({filesToBeSent, filesPreview});
}
render() {
return (
<Card shadow={0} style={{ margin: '10px'}}>
<CardTitle>Add Music Resources</CardTitle>
<CardText >
<form id="upload-form">
<Dropzone
onDrop={(files) => this.onDrop(files)}
multiple={true}
>
<div>Try dropping some files here, or click to select files to upload.</div>
</Dropzone>
<div>
Files to be uploaded are:
{this.state.filesPreview}
</div>
<input className={this.state.buttonClasses} type="submit" value="Submit" />
</form>
</CardText>
</Card>
);
}
}
export default AddMusicResource;
So basically I'm taking a file input and then pushing the name of the file to a filePreview array that is being rendered on the page, but when a user clicks the close x to the right of the name I expect that the name of the file would be removed from the DOM. The removeFile() is working as intended and is indeed removing the correct element from the page, but the only time that the DOM would update to see that file as removed is if I added another file using the Dropzone.
I was wondering if anyone could help me figure out why the DOM is not rerendering at the end of my removeFile() function?
I have tried both this.setState(this.state) and this.forceUpdate() but neither have been successful.
You are mutating the state and React has a hard time to find what actually changed. Try this instead:
removeFile(file, event) {
var i = this.state.filesToBeSent.indexOf(file);
if (i < 0)
return;
this.setState((prevState) => {
return {
filesToBeSent: prevState.filesToBeSent.filter((element, index) => index !== i),
filesPreview: prevState.filesPreview.filter((element, index) => index !== i)
};
});
}
I am new to ReactJS and I was wondering what is the correct way to target next element with same class in react?
<div className="portfolioGallery">
<img className="portfolioImg activeImg" src="img/1.png"/>
<img className="portfolioImg" src="img/2.png"/>
<img className="portfolioImg" src="img/2.png"/>
<div className="portfolioNext" onClick={this.nextImg.bind(this)}>
Next image
</div>
</div>
What would be the correct way that when I click the portfolioNext div I would be able to give the img2 class of activeImg and remove it from the previous element and so on in ReactJS?
Thank You!
constructor() {
super();
this.state = {
default: "portfolioImg activeImg"
};
}
nextImg() {
this.setState({
default: "portfolioImg"
});
}
That's the kind of imperative technique that you'd generally find in jQuery code, but it doesn't map very well to React's slightly more declarative nature.
Rather than trying to find the next element with a class, use state to maintain a list of those elements alongside an index cursor.
// constructor
this.state = {
images = ['img/1.png', 'img/2.png', 'img/3.png']
cursor: 0
};
Then use these bits of data to render your view.
// render
const { images, cursor } = this.state;
return (
<div>
{images.map((src, index) => {
const activeClass = (index === cursor) ? 'activeImg' : '';
return <img className={`portfolioImg ${activeClass}`} />;
}}
</div>
);
To change the active image, use setState to change the cursor property.
// nextImg
const { cursor, images } = this.state;
const nextCursor = cursor % images.length;
this.setState({ cursor: nextCursor });
I wouldn't suggest you think of it as siblings finding each other, but instead thing of it as the parent storing an index of the current children, and updating that instead.
this.props (or this.state) would have something like this (pseudocode)
this.props.images => ["img/1.png", "img/2.png", "img/2.png"];
Inside render:
<div className="portfolioGallery">
{this.props.images.map((image, i ) => active === i ? (<img className="portfolioImg activeImg" src="image" key={i}/>) : (<img className="portfolioImg " src="image" key={i}/>))}
<button className="portfolioNext" onClick={(e) => this.setState({active: this.state.active + 1}).bind(this)}>Next image</button>
</div>
Of course, accounting for when active >= images.length, but you get the idea
I'm trying to add a class to an element within ReactJS using the map function, but ONLY for the first one in the loop, is this possible / an easy way?
return (
<div key={itemData.itemCode} className="item active">
Want to add 'active' class when the first but for the others dont add it
</div>
)
If you use .map or .forEach then you can do it like this
var List = React.createClass({
render: function() {
var lists = this.props.data.map(function (itemData, index) {
/// if index === 0 ( it is first element in array ) then add class active
var cls = (index === 0) ? 'item active' : 'item';
return <div key={itemData.itemCode} className={ cls }>
{ itemData.itemValue }
</div>;
})
return <div>{ lists }</div>;
}
});
Example
also there is good package called classnames if you need conditionally change classes, like as in your case
var List = React.createClass({
render: function() {
var lists = this.props.data.map(function (itemData, index) {
return <div
key={itemData.itemCode}
className={ classnames('item', { active: index === 0 }) }>
{ itemData.itemValue }
</div>
})
return <div>{ lists }</div>;
}
});
Example