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.
Related
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.
I am trying to update my data dynamically only on frontend. I am having list of objects and I am mapping through them.
To be more specific, I have some chairs as my products and different chair has different color variants.
If I click on chair n. 1, it will be selected and chair variants will be shown, this should be changed when chair n.2 is clicked.
const ProductsData = Object.values(products).map((data) => {
return (
<>
<div className="box" onMouseEnter={() => setSelectedData(data)}>
<img src={data.image} />
<Row>
<h1>{data.name}</h1>
</Row>
</div>
</>
);
});
And data of selected chair:
<div className="container">
{ProductsData}
</div>
and showing different variants of chair colors
{selectedData ? (
{selectedData.name}
{selectedColor && <img src={selectedColor.image} alt="chair" />}
{Object.values(selectedData.chair.colors).map((val) => {
return <div onClick={() => setSelectedColor(val)}>{val.name}</div>;
})}
)}
Here is created sandbox for easier understanding: https://codesandbox.io/s/friendly-meninsky-ypjmr?file=/src/App.js:3483-3708
Issues that needs to be solved:
Clicking on ONE should show default image, right now user has to click on color variant to show the image. -> When clicking on TWO, image should get updated with default image of TWO, right now image still stays from ONE until color variant of TWO is clicked.
In summary listItemsInCart() both options ONE and TWO are having the same image, even though each of the option should have its own image of the color variants that has been picked.
Is it possible to achieve this with sharing one state?
const [selectedData, setSelectedData] = React.useState();
const [selectedColor, setSelectedColor] = React.useState();
To select the default color upon choosing item, change onClick handler like this:
onClick={() => {
setSelectedData(data);
setSelectedColor(Object.values(data.chair.colors)[0]);
}
Colors should ideally be an array since every item will have same structure, then it'd be:
setSelectedColor(data.chair.colors[0]);
I want to add underline to menu item when its active. All works fine, but when I click on an item, its previous classes received from the ReactTransitionGroup add-on component are reset. For example when I click second item the classes will be reset and only active will remain. I want the active class to be insert to existing without cleaning the previous ones.
The .active has ::after pseudo-class
const NavItems = (props) => {
const items = ["section1", "section2", "section3", "section4", "section5"];
const [activeItem, setActive] = useState(0);
return (
<>
<NavItemsOverlay open={props.open} />
<ScollLinks open={props.open}>
{items.map((item, index) => {
return (
<CSSTransition
in={props.open}
key={index}
timeout={{enter: 100 * index, exit: 0 }}
classNames="fade ">
<Link
className={activeItem === index ? " active" : ""}
onClick={() => setActive(index)} >
{item}
</Link>
</CSSTransition>
);
})}
</ScollLinks>
</>
);
};
You appear to be running into issue #318, which is still open. The person posting the issue thinks it's a bug, and the CSSTransition documentation does say:
A few details to note about how these classes are applied:
They are joined with the ones that are already defined on the child component, so if you want to add some base styles, you can use className without worrying that it will be overridden.
...so yeah, that sounds like a bug.
The best way to solve it would be to fork the project, fix the bug, and send a PR. :-)
A really hacky way to work around it would be to use a data-* attribute instead of a class:
<Link
data-cls={activeItem === index ? " active" : ""}
onClick={() => setActive(index)} >
{item}
</Link>
And then in the CSS, instead of:
.active::after {
/* ... */
}
You'd have
[data-cls~=active]::after {
/* ... */
}
That uses class-like attribute matching to match that element.
I am trying to use a semantic UI accordion containing multiple entries and allow more than one entry to be open at once; each entry has a title portion containing an icon with a popup attached and a content area containing a textarea.
I would like to be able to have both accordions open at the same time which is apparently supported by using the exclusive={false} prop when making the accordion element as described in the documentation here
But that example looks to be using an array of objects with content that is a string, not other react/html/jsx elements (in my case it is semantic ui icons, popups and textareas). That array of objects is passed in to the accordion's panel prop.
And I am unfamiliar with what the semantic ui react accordion requires to function correctly with keeping track of indices and other stuff, I am not sure what else I need to configure or if this is possible with the semantic ui component as is.
I essentially copied this example and use an active index and an onclick handler which switches the active index in the component react state.
Here is a snippet of the accordion and onclick handler and react app state:
class FileUpload extends Component {
// other stuff omitted
constructor(props) {
super(props);
this.state = {
activeAccordionIndex: -1
};
handleAccordionClick = (e, titleProps) => {
const { index } = titleProps;
const { activeAccordionIndex } = this.state;
const newIndex = activeAccordionIndex === index ? -1 : index;
this.setState({
activeAccordionIndex: newIndex
})
}
// I'm using a small helper function to create the accordion and invoke it in
// the render method, just one item for brevity; the other entries are pretty
// much the same
getAccordionInputs() {
const { activeAccordionIndex } = this.state;
let accordionContent = (
<Accordion fluid exclusive={false}>
<Accordion.Title
className="file-upload-ordinal-accord-title"
active={activeAccordionIndex === 0}
index={0}
onClick={this.handleAccordionClick}
>
<Icon name='dropdown' />
Enter Ordinal Features
<Popup
on="click"
position="right center"
header="Ordinal Features Help"
content={
<div className="content">
<p>Ordinal Features help description</p>
</div>
}
trigger={
<Icon
className="file-upload-ordinal-help-icon"
inverted
size="large"
color="orange"
name="info circle"
/>
}
/>
</Accordion.Title>
<Accordion.Content
active={activeAccordionIndex === 0}
>
<textarea
className="file-upload-ordinal-text-area"
id="ordinal_features_text_area_input"
label="Ordinal Features"
placeholder={"{\"ord_feat_1\": [\"MALE\", \"FEMALE\"], \"ord_feat_2\": [\"FIRST\", \"SECOND\", \"THIRD\"]}"}
onChange={this.handleOrdinalFeatures}
/>
</Accordion.Content>
</Accordion>
)
return accordionContent;
}
}
I don't know how to set this up to allow multiple accordions open at once with content that is not a string. Is this possible with the semantic ui accordion? Or do I need to find an alternative solution and/or make the piece with the desired behavior by hand?
You can change your index logic so instead of setting the active index in your state add the index to an array and check if it exists inside the array and if it does show that accordion
Here is an example:
export default class AccordionExampleStandard extends Component {
state = { activeIndexs: [] };
handleClick = (e, titleProps) => {
const { index } = titleProps;
const { activeIndexs } = this.state;
const newIndex = activeIndexs;
const currentIndexPosition = activeIndexs.indexOf(index);
if (currentIndexPosition > -1) {
newIndex.splice(currentIndexPosition, 1);
} else {
newIndex.push(index);
}
this.setState({ activeIndexs: newIndex });
};
render() {
const { activeIndexs } = this.state;
return (
<Accordion>
<Accordion.Title
active={activeIndexs.includes(0)}
index={0}
onClick={this.handleClick}
>
<Icon name="dropdown" />
What is a dog?
</Accordion.Title>
<Accordion.Content active={activeIndexs.includes(0)}>
<p>
A dog is a type of domesticated animal. Known for its loyalty and
faithfulness, it can be found as a welcome guest in many households
across the world.
</p>
</Accordion.Content>
<Accordion.Title
active={activeIndexs.includes(1)}
index={1}
onClick={this.handleClick}
>
<Icon name="dropdown" />
What kinds of dogs are there?
</Accordion.Title>
<Accordion.Content active={activeIndexs.includes(1)}>
<p>
There are many breeds of dogs. Each breed varies in size and
temperament. Owners often select a breed of dog that they find to be
compatible with their own lifestyle and desires from a companion.
</p>
</Accordion.Content>
<Accordion.Title
active={activeIndexs.includes(2)}
index={2}
onClick={this.handleClick}
>
<Icon name="dropdown" />
How do you acquire a dog?
</Accordion.Title>
<Accordion.Content active={activeIndexs.includes(2)}>
<p>
Three common ways for a prospective owner to acquire a dog is from
pet shops, private owners, or shelters.
</p>
<p>
A pet shop may be the most convenient way to buy a dog. Buying a dog
from a private owner allows you to assess the pedigree and
upbringing of your dog before choosing to take it home. Lastly,
finding your dog from a shelter, helps give a good home to a dog who
may not find one so readily.
</p>
</Accordion.Content>
</Accordion>
);
}
}
https://codesandbox.io/s/xo226wp5lw?module=example.js
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.