Conditionally rendering contents of a button - javascript

I am making a memory game, CodeSandbox, and I am trying to accomplish the following:
Get the cards to be dealt initially face down (icon hidden so to speak)
onClick, the icon on the card that is clicked is revealed, none of the others is revealed
When the second card is clicked that icon is revealed, if they match the cards are removed anyway (that part works) but if they don't, the icons are hidden again.
I tried adding a "buttonMask" class in the parent div but it overrides my existing class. I tried display: none in the parent div also, no luck.
Other attempts included adding a state property to the Card.js component this.state.isHidden: true and trying to toggle the visibility. I read the docs, This, SO, and eddyerburgh. I'm stuck on this condition render, everything else works great, this piece got me
This is my conditional for dealing the cards
class Cards extends Component {
constructor(props) {
super(props);
}
render() {
const card = (
<div style={sty}>
{this.props.cardTypes ? (
this.props.cardTypes.map((item, index) => {
return (
<button style={cardStyle} value={item} key={index}>
{" "}
{item}{" "}
</button>
);
})
) : (
<div> No Cards </div>
)}
</div>
);
return <Card card={card} removeMatches={this.props.removeMatches}
/>;
}
Originally, when I first tried this, I was passing state to all the cards, not just individual cards, that was why I was not able to toggle them individually.
This was the original render in Cards.js
if (this.state.hidden) {
return (
<button
key={card + index}
style={btn}
onClick={this.revealIcon}
id={card}
>
<div style={btnMask}>{card}</div>
</button>
);
} else {
return (
<button
key={card + index}
style={btn}
onClick={this.revealIcon}
id={card}
>
<div>{card}</div>
</button>
);
}

You can conditionally render an element with an inline if with logical &&:
return(
<div>
{this.props.shouldRenderTheThing &&
<span>Conditionally rendered span</span>}
</div>
);

Related

How to trigger a specific element in a list of elements that are created using .map in React JS?

I'm trying to make an accordion, that opens up upon clicking the "+" icon. But when I click on a single element, all the other elements expand. Is there a way to just open one of the accordions? I've added the screenshot of the accordion and also added the code.
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
const [display, setDisplay] = useState(false);
const clickHandle = () => {
setDisplay(!display);
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{data.map((question) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
<button className="btnContainer" onClick={clickHandle}>
{display === false ? (
<AiFillPlusCircle size="2.4em" />
) : (
<AiFillMinusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{display === false ? (
<></>
) : (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Explanation
Basically you can't use a single variable to toggle all your elements. All the elements will act as a single element, so either all will open or all will close.
You need something that can be checked against each element. Now id is a potential candidate but it has it's drawbacks, since it's a list the best option is using the index of the element itself.
So you first change the display type from boolean (false) to integer type and default it to -1 (anything less than zero)
Then change your .map function from .map((question) =>... to .map((question, questionIndex) =>..., this will get you a variable questionIndex which holds the current question's index
You can use that (questionIndex) and the display variable to check against each other and display the appropriate states.
Benefits when compared to other answers
Since you are dealing with a list of items, it is always best to use the index of an element to toggle the element's display, This ensures you have decoupled your View from your Data. (As much as possible)
If for some reason your id is null or duplicate, it will create issues in your display.
It is easier to just call toggleElement(2) to automatically open an element for a given position via code (on first load). This is useful if you want to maintain the open states between url changes / reloads, you just add the index to the query parameter of the url.
Solution
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
// Update the display to be of type integer and init it with -1
const [display, setDisplay] = useState(-1);
// Add a parameter to the click function to take the clicked element's index
const toggleElement = (currentIndex) => {
// Check if the element that is clicked is already open
if(currentIndex===display) {
setDisplay(-1); // If it is already open, close it.
}
else {
setDisplay(currentIndex); // else open the clicked element
}
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{/* Add a variable questionIndex to the map method to get the index of the current question */}
{data.map((question, questionIndex) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
{/* Update the button onClick event to pass the current element's index via the questionIndex variable */}
<button className="btnContainer" onClick={()=> toggleElement(questionIndex)}>
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<AiFillMinusCircle size="2.4em" />
) : (
<AiFillPlusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
) : (
<></>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Instead of a boolean, use an id in your state
const [display, setDisplay] = useState("");
When you map an item, add this function
const { id, title, info } = question;
const handleSetDisplay = () => {
if(id === display) {
//Close panel
setDisplay("")
} else {
//open specific panel
setDisplay(id)
}
}
Adjust the button's onClick
<button className="btnContainer" onClick={handleSetDisplay}>
Then to compare if your panel should expand, use
{display === id ? ///...: ...}
In short, you need to compare the saved ID with the mapped item's id.
If your id is a number, just change the initial state to 0

How to add new class without reseting previous ones in React

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.

Display element for one person on hover

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.

How to have multiple accordions open with semantic UI react?

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

Toggle prop passed on one of many targets in ReactJS

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.

Categories

Resources