I am making a drag n drop sort of game where you match the logos with their corresponding name.
If user matches the logo with the name correctly than the field which you could drop the logo gets additional classes.
Like this:
if (isCorrectMatching) {
event.target.classList.add("dropped");
draggableElement.classList.add("dragged");
event.target.classList.add("dragged");
event.target.setAttribute("draggable", "false");
draggableElement.setAttribute("draggable", "false");
event.target.innerHTML = `<i class="fab fa-${draggableElementBrand}" style="color: ${draggableElement.style.color};"></i>`;
}
If every match is found user can go to next level , my problem is that these additional classes are staying there , how do I remove them ?
I am mapping them out like this:
<div className="containerItems">
{draggableItems.map((x, i) => {
return (
<div className="draggable-items">
<i
onDragStart={(e) => dragStart(e)}
className={`draggable fab fa-${x}`}
id={x}
draggable="true"
ref={draggableOnes.current[i]}
></i>
</div>
);
})}
</div>;
{
matchingPairs.map((x, i) => {
return (
<section className="matching-pairs">
<div className="matching-pair">
<span className="label">{x}</span>
<span
className="droppable"
// ref={droppableOnes.current[i]}
onDragEnter={(e) => dragEnter(e)}
onDragOver={(e) => dragOver(e)}
onDragLeave={(e) => dragLeave(e)}
onDrop={(e) => drop(e)}
data-brand={x}
></span>
</div>
</section>
);
});
}
I can not seem to solve this one, like how do I remove all the classes that I've added when there was a correct matching.
I would like to remove basically everything that I've added in my if (isCorrectMatching) .
I've tried to use refs but it did not work. What is the way to go for this?
In React, you don't directly manipulate DOM elements (well, almost never), including their the class lists. Instead, you keep your state information in the component and use that state information to render the elements that make up your component (including their classes). React will then compare the rendered elements you return with the DOM and make any necessary changes (such as updating the classList). So in your code, when you see that you have a correct matching, you wouldn't directly modify those DOM elements' classList lists, you'd update your state to remember the match, and use that state information in the next render to put the appropriate classes on the elements being rendered.
Here's a simpler example with a tickbox, but it's the same general concept:
const {useState} = React;
const Example = () => {
const [isChecked, setIsChecked] = useState(false);
return <div>
<label>
<input
type="checkbox"
checked={isChecked}
onChange={() => setIsChecked(flag => !flag)}
/>
Ticked
</label>
<div className={isChecked ? "yes" : "no"}>
Example
</div>
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
.yes {
color: green;
}
.no {
color: #d00;
}
label {
user-select: none;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Note how the state member isChecked determines what classes the div has, and is updated by ticking/unticking the checkbox.
Related
I am new to React and I am trying to build a simple todolist app.
Once a task is inserted, a div with an unchecked checkbox appears. Now, I'd like to hide or remove the div when the checkbox is clicked. I think I hooked up everything correctly but I miss, conceptually, what is the best way to do it: should I make a className attribute that changes upon clicking the checkbox? Should I modify the tasks array containing by removing the task that has been checked? Or, is there any other good way I could achieve this?
Thank you so much for your help.
https://scrimba.com/scrim/cof7e4dde89963549cc216a25 Here's the directly editable code
.insert {
margin: 0 auto;
display: block;
padding: 20px;
}
.enter--task {
border-radius: 5px;
}
.tasks {
width: 40%;
min-height: 100%;
color: black;
background-color: white;
margin-top: 20px;
border-radius: 15px;
line-height: 40px;
padding: 10px;
overflow-wrap: break-word;
}
.task--check {
margin: 5px;
}
<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>
import React from "react"
export default function Insert () {
const [task, setTask] = React.useState ("") // settin state for the single task, e.g. the input.
const [taskArray, setTaskArray] = React.useState ([]) // setting state for the array of tasks.
const tasks = taskArray.map((tasks) => {
// {event.target.checked ? "hidden": "tasks"} this on className did not work.
return (
<div className="tasks">
<input
type="checkbox"
className="task--check"
onChange={handleCheck}
/>
{tasks}
</div>
)
}) // This is basically to return a series of paragraphs (soon divs) once enter or add task are clicked.
//How to let the task div disappear upon checking the checkbox?
function handleCheck() {
console.log(event.target.checked ? "checked": "unchecked")
}
function handleChange (event) {
setTask(event.target.value)
} // this is a function to register the value of the input. We use the setTask function to update the value of task to the value present in the input, which in this case it is our target
function handleSubmit (event) {
event.preventDefault()
setTaskArray(task ? [...taskArray,task] : [...taskArray]) // quite hyped! Problem: user can input empty tasks. Solution: ternary operator saying, if task is true (truthy in this case, meaning a string with some content), add it to the taskArray. Otherwise, just return taskArray without appending the falsy task (empty string).
setTask("")
}
// this function handles the event after clicking enter or add task. First, we use event.preventDefault(): this is to avoid that every time that we click enter or add task, the page refreshes and the input is attached to the url (try to remove and see what happens). Then we have to make another array to insert the new task. To do this, we spread the previous array (taskArray) and we add the new element, which is task. After that, we reset the value of task to an empty string - this is to show the placeholder again.
return (
<main className="insert">
<form onSubmit={handleSubmit}>
<input
type="text"
className="enter--task"
placeholder="Enter your task"
onChange={handleChange}
name="task"
value={task} // setting the value of my input equal to my state
/>
<button>Add task</button>
</form>
{tasks}
</main>
)
}
Create a state called checkState
const [checkState, setCheckState] = React.useState(false)
const handleCheck = (event) => {
setCheckState(event.checked);
}
<div className="tasks">
<input
type="checkbox"
checked={checkState}
onChange={handleCheck}
/>
{tasks}
</div>
{checkState && <div>Show div when checkbox checked</div>}
I guess it depends on what you want to achieve, in the classic todo list example, we would remove the task from the state. By hiding it, you would keep the reference in memory. I'm not sure to see a good use case for it.
If you want to remove, you could do it like this. Note that I'm using the index as key which is not recommended as the order of the items could change, instead you should add an id attribute to your tasks, and use it as the key. You will also use it to remove the task.
const tasks = taskArray.map((task, i) => {
return (
<div className="tasks" key={i}>
<input
type="checkbox"
className="task--check"
onChange={() => removeTask(i)}
/>
{task}
</div>
)
})
function removeTask(index) {
setTaskArray(prev => prev.filter((_,i) => i !== index))
}
I'm trying to change the text of a <p> to David and Larry accordingly when each button (that has an image inside) has hovered. I have experimented with numerous things and found a way to change the CSS of the button with a function. But I was unable to find anything to change text since <p> is in a different class. Any suggestions to address this problem?
For your information, I have added a CSS color changing function I used earlier to the below code sample.
here's my code.
import React from 'react';
import "./Tri.css";
function Tri() {
function davidon(e) {
e.target.style.background = 'red';
}
function davidoff(e) {
e.target.style.background = 'green';
}
function larryon(e) {
e.target.style.background = 'red';
}
function larryoff(e) {
e.target.style.background = 'green';
}
return (
<div>
<div>
<div>
<button onMouseOver={davidon} onMouseLeave={davidoff}>
<img src={require(`./images/david.png`)} className="david"/>
</button>
<button onMouseOver={larryon} onMouseLeave={larryoff}>
<img src={require(`./images/larry.png`)} className="larry"/>
</button>
</div>
<div className="plex">
<p>Larry Or David?</p>
</div>
</div>
</div>
);
}
export default Tri;
Thanks in advance for you replies.
You need to think more in "React", and use component state and props. The offical documentation is a good place to start.
Here I've got two components.
1) Tri: which has it's own state, and builds the HTML using Button components
2) Button: since you need each button to change color depending on the mouse action it's best to separate that functionality out into a new component so that each instance can have its own state.
(I've intentionally left out the images in this example, but you could pass in a src prop to the button and have that handle the images too if you wanted.)
const { useState } = React;
// `Button` accepts a props object
// Here I've destructured out the button name,
// and the handleHover function
function Button({ name, handleHover }) {
// We initialise the state with "green"
const [ color, setColor ] = useState('green');
function handleColor() {
// We set the new color based on the current color
setColor(color => color === 'red' ? 'green' : 'red');
// And then call the `handleHover` function, passing in `name`
handleHover(name);
}
return (
<button
className={color}
onMouseOver={handleColor}
onMouseLeave={handleColor}
>
{name}
</button>
);
}
function Tri() {
// In `Tri` we set its own state for the name
// initialised to an empty string
const [ name, setName ] = useState('');
// A handler that changes the name
// This is the function we pass to each button
function handleHover(name) {
setName(name);
}
// Set up two buttons using our Button component
// assigning a name to each, and passing in our handler
// Whenever the name (state) is changed the name in the
// paragraph also changes
return (
<div>
<div>
<Button name="Larry" handleHover={handleHover} />
<Button name="David" handleHover={handleHover} />
</div>
<p>{name}</p>
</div>
);
}
ReactDOM.render(
<Tri />,
document.getElementById('react')
);
.red { background-color: red; }
.green { background-color: green; }
<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>
Try using states. And don't change DOM-nodes dynamically in event handlers. Always use React functionality:
React uses a declarative form of programming (The Virtual DOM specifically). You define variables and set them and React updates the DOM if those change.
useState gives you the opportunity to declare an editable (through a setter function) variable. See Docs on State and Props.
import React from 'react';
import "./Tri.css";
function Tri(props) {
// props as immutable arguments (if needed)
// useState keeps an internal state in the component
let initialTxt = 'Larry Or David?';
const [text, setText] = React.useState(initialTxt);
return (
<div>
<div>
<div>
<button
className="david-btn"
onMouseOver={() => setText('David')}
onMouseLeave={() => setText(initialTxt)}>
<img src={require(`./images/david.png`)} className="david"/>
</button>
<button
className="larry-btn"
onMouseOver={() => setText('Larry')}
onMouseLeave={() => setText(initialTxt)}>>
<img src={require(`./images/larry.png`)} className="larry"/>
</button>
</div>
<div className="plex">
<p>{text}</p>
</div>
</div>
</div>
);
}
Also, extend ./Tri.css with the following code. You could use a style-variable but that would make your code more bloated and unreadable if you have access to CSS.
.david-btn,
.larry-btn {
background-color: green;
}
.david-btn:hover,
.larry-btn:hover {
background-color: red;
}
You are looking for Refs. You can read more about them in documentation.
I've created a simple example (based on your code).
Step by step what I did:
import useRef hook which is used to create reference.
import React, { useRef } from "react";
created reference:
const pTagRef = useRef();
passed reference to your p tag
<div ref={pTagRef} className="plex">
<p>Larry Or David?</p>
</div>
created function which can change the content of this reference where pTagRef.current is DOM element.
function setName(name) {
if (pTagRef.current) {
pTagRef.current.innerText = name;
}
}
called the function whenever name changed
setName("larry");
You should definitely use state for this but I hope this one helps you to get started.
I am rendering a list of students, some of which have failed their exams. For those who have failed their exams, I display a red square behind their avatars.
Whenever I hover over a student's avatar I want to display the subject that student has failed. My issue at the moment is that I display the subjects for all students, not only the one I've hovered over.
How can I display only the mainSubject for the student who's avatar I hovered on?
Here is a link to my code sandbox: Example Link
I solved it like following.
Get the id of the hovered student. Match this id from the list of students you render. if its match then show the subjects
Also, I renamed the hook
add key prop
you can check this too https://codesandbox.io/s/zealous-bhaskara-mi83k
const [hoveredStudentId, setHoveredStudentId] = useState();
return (
<>
{students.map((student, i) => {
return (
<div className="student-card" key={i}>
<p>
{student.firstName} {student.lastName}
</p>
{student.passed === false ? (
<>
<img
id={student.id}
src={student.picture}
className="student-avatar fail"
onMouseEnter={e => {
setHoveredStudentId(e.currentTarget.id);
}}
onMouseLeave={e => {
console.log(e.currentTarget.id);
setHoveredStudentId(0);
}}
alt="avatar"
/>
{hoveredStudentId === student.id && (
<div className="subject-label">{student.mainSubject}</div>
)}
</>
) : (
<img
src={student.picture}
className="student-avatar"
alt="avatar"
/>
)}
</div>
);
})}
</>
);
Issue is that you have a list of students but only 1 flag to show/hide subjects.
Solution: 1
Maintain a list of flag/student. So you will have n flags for n students. Simple way for this is to have a state in a way:
IStudentDetails { ... }
IStudentStateMap {
id: string; // uniquely identify a syudent
isSubjectVisible: boolean;
}
And based on this flag isSubjectVisible toggle visibility.
Updated code
Solution 2:
Instead of handling it using React, use CSS tricks. Note this is a patch and can be avoided.
Idea:
Wrap Student in a container element and add a class onHover on elements on elements that needs to be shown on hover.
Then use CSS to show/hide those elements.
.student-container .onHover {
display:none;
}
.student-container:hover .onHover{
display: block;
}
This way there wont be rerenders and no need for flags.
Updated Code
However, solution 1 is better as you have more control and when you are using a UI library, its better to let it do all mutation and you should follow its ways.
I display a list of foos and when i click on some link more results i keep the existing foos and i append to them the new ones from my api like bellow
const [foos, setFoos] = useState([]);
...
// api call with axios
...
success: (data) => {
setFoos([ ...foos, ...data ])
},
Each <Foo /> component run the animation above
App.js
...
<div className="foos-results">
{ foos.map((foo, index) => <Foo {...{ foo, index }} key={foo.id}/>) }
</div>
...
Foo.js
const Foo = ({ foo, index }) => <div className="circle">...</div>
animation.css
.circle {
...
animation: progress .5s ease-out forwards;
}
The problem is when i append the new ones then the animation is triggered for all the lines of <Foo />.
The behavior expected is that the animation is triggered just for the new ones and not starting over with the existing ones too.
UPDATE
We have found the origin of the problem (it's not related to the uniqueness of key={foo.id})
if we change
const Foo = ({ foo, index }) => <div className="circle">...</div>
to
const renderFoo = ({ foo, index }) => <div className="circle">...</div>
And App.js to
...
<div className="foos-results">
{ foos.map((foo, index) => renderFoo({ foo, index })) }
</div>
...
It works
So why is this behavior like this in react ?
here is a sandbox based on #Jackyef code
This is quite an interesting one.
Let's look at the sandbox provided in the question.
Inside App, we can see this.
const renderItems = () => (
<div>
{items.map((item, index) => (
<div className="item" key={item.id}>
<span>
{index + 1}. {item.value}
</span>
</div>
))}
</div>
);
const Items = () => renderItems();
return (
<div className="App">
<h1>List of items</h1>
<button onClick={addItem}>Add new item</button>
<Items />
</div>
);
Seems pretty harmless right? The problem with this is that Items is declared in the App render function. This means that on each render, Items actually is now a different function, even though what it does is the same.
<Items /> is transpiled into React.createElement, and when diffing, React takes into account each components' referential equality to decide whether or not it is the same component as previous render. If it's not the same, React will think it's a different component, and if it's different, it will just create and mount a new component. This is why you are seeing the animation being played again.
If you declare Items component outside of App like this:
const Items = ({ items }) => (
<div>
{items.map((item, index) => (
<div className="item" key={item.id}>
<span>
{index + 1}. {item.value}
</span>
</div>
))}
</div>
);
function App() { /* App render function */}
You will see everything works as expected. Sandbox here
So, to summarise:
Referential equality matters to React when diffing
Components (function or class that returns JSX) should be stable. If they change between renders, React will have a hard time due to point number 1.
I don't think there is a way to disable this re-rendering animation, but I think there is a workaround that could solve this issue.
As we know that each div's css is reloaded every time, so the solution I can think of, is to create another css class rule (let this class be named 'circle_without_anim') with same css as class 'circle' but without that animation and while appending new div, just before appending change class of all divs that have class name 'circle' to 'circle_without_anim' that would make the changes and css to previous divs but just without that animation and the append this new div with class 'circle' making it the only div that have animation.
Formally the algorithm will be like:
Write another css class(different name for example prev_circle) with same rules as 'circle' but without the animation rule.
In Javascript just before appending new div with class 'circle', change class of all previous divs that have class named 'circle' to newly created class 'prev_circle' that do not have animation rule.
Append the new div with class 'circle'.
Result: It would give an illusion that the CSS of previous divs is not being reloaded as the css is same but without animation, but the new div has different css rule (animation rule) which is going to be reloaded.
With this code:
const Items = () => renderItems();
...
<Items />
React has no chance of knowing that Items in the current render is the same component as Items in the previous render.
Consider this:
A = () => renderItems()
B = () => renderItems()
A and B are different components, so if you have <B /> in the current render and <A /> instead of <B /> in the previous render, React will discard the subtree rendered by <A /> and render it again.
You are invoking React.createElement (since <Items /> is just a JSX syntax sugar for React.createElement(Items, ...)) every render, so React scraps the old <Items /> in the DOM tree and creates it again each time.
Check out this question for more details.
There are two solutions:
create Items component outside of the render function (as Jackyef suggested)
use render function ({ renderItems() } instead of <Items />)
Just starting off with ReactJS and have a project where I am showing an accordion of issues and including a details area that is hidden on the start.
There is a button in the accordion bar that should pass a prop to the child element to hide or show them. I have refs on the button and on the details child compoment and added a function to call the function and pass the ref of the details area. I am just not sure how to dynamically change the class hidden on one of many areas and not all of them.
Not sure if putting a class on each element and then learning how to toggle the particular child's class is better or changing the prop to the child.
I can get to the change function but am drawing a blank from there and all the googling shows how to do one element with a grand change of state but I need individual elements.
Here is what I have so far.
Parent
...
<AccordionItem key={item.id} className={iconClass} title={`${item.area}`} expanded={item === 1}>
{
item.issues.map(issue => {
let trim = (issue.issue.length>21) ? `${issue.issue.substring(0,22)}...`: issue.issue;
return (
<div className="issue-bar container-fluid">
<div className="row issue-bar-row">
<span className="issue-title"><img src={CriticalRed} alt="Critical"/> {trim}</span>
<span className="btns">
<button className="btn btn-details" onClick={() => this.showDetail(`details-${issue.id}`)}>Details</button>
</span>
</div>
<IssuesDetails ref={`details-${issue.id}`} issue={issue} shouldHide={true} />
</div>
)
})
}
<div>
</div>
</AccordionItem>
...
Child
export default class IssuesDetails extends Component{
render(){
let issueDetails = classNames( 'issue-details', { hidden: this.props.shouldHide } )
return(
<div className={issueDetails}>
<div className="issues-details-title">
<h3>{this.props.issue.issue}</h3>
</div>
<div className="issues-details-details">
{this.props.issue.details}
</div>
<div className="issues-details-gallery">
<ImageGallery source={this.props.issue.photos} showPlayButton={false} useBrowserFullscreen={false} />
</div>
<button className="btn btn-success">Resolve</button>
</div>
)
}
}
Thanks for any help you provide or places you can send me!
If i'm understanding correctly, you need to be able to swap out shouldHide={true} in certain circumstances. To do this, you'll want your parent component to have a state object which indicates whether they should be hidden or not.
Exactly what this state object looks like depends on what sort of data you're working with. If the issues is a single array, then perhaps the state could be an array of booleans indicating whether each issue is expanded or not. I suspect you may have a more nested data structure, but i can't tell exactly since some of the code was omitted.
So assuming you have an array, it might look like this (i've omitted some things from the render method for brevity):
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
hidden: (new Array(props.issues.length)).fill(false),
};
}
showDetail(index) {
let newHidden = this.state.hidden.slice();
newHidden[index] = true;
this.setState({
hidden: newHidden
});
}
render() {
return (
<AccordionItem>
{this.props.issues.map((issue, index) => {
<div>
<button onClick={() => this.showDetail(index))}/>
<IssuesDetails issue={issue} shouldHide={this.state.hidden[index]}/>
</div>
})}
</AccordionItem>
);
}
}
Take a look at these:
https://codepen.io/JanickFischr/pen/xWEZOG
style={{display: this.props.display}}
I think it will help with your problem. If you need more information, please just ask.