Setting Selected State to Mapped Components - javascript

I have a mapped component which iterates through API data. It passes props to each one and therefore each card looks different. See example below.
https://gyazo.com/39b8bdc4842e5b45a8ccc3f7ef3490b0
With the following, I would like to achieve two goals:
When the component is selected, it uses state to STAY SELECTED, and changes the colour as such to lets say blue for that selected component.
I hope this makes sense. How do I index a list as such and ensure the colour and state remains active based on this selection?
See below.
The level above, I map the following cards using these props.
{
jobs.length > 0 &&
jobs.map(
(job) =>
<JobCard key={job.id} job={job}
/>)
}
I am then using the following code for my components:
const JobCard = ({ job }) => {
const responseAdjusted = job.category.label
const responseArray = responseAdjusted.split(" ")[0]
return (
<CardContainer>
<CardPrimary>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1">
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" fontSize="inherit">
<FavoriteIcon fontSize="inherit"
onClick={()=> setOpen(prevOpen => !prevOpen)}/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color="text.secondary">
{job.company.display_name}
</Typography>
</CardHeader>
<CardSecondary>
</CardSecondary>
</CardPrimary>
</CardContainer>
)
}

You can attach a handler on the <CardPrimary> component by passing a function to the onClick event. That way whenever you click anywhere on the card div, the function will be triggered.
const [isSelected, setIsSelected] = useState(false);
<CardPrimary onClick={() => setIsSelected(true)} className={isSelected ? "css-class-to-highlight-div" : undefined>
....
</CardPrimary>

If I'm understanding what you're asking for, which I believe is to have your component be highlighted when it is clicked, then you need to modify the 'CardContainer' component to render with an 'onClick' parameter.
Example:
function CardContainer(props) {
const cssClass = 'highlighted';
const my_id = props.id || 'need_an_id';
var clearExistingHighlight = () => [...document.getElementByClassName(cssClass)].forEach((elem)=>elem.classList.remove(cssClass));
var isHighlighted = () => document.getElementById(my_id).classList.contains(cssClass);
var setHighlighted = (e) => {
clearExistingHighlight();
e.target.classList.add(cssClass);
}
return (
<div id={my_id} onClick={setHighlighted}>Cheeseburger fry</div>
)
}
If you don't want the highlight to disappear, you can get rid of the clearExistingHighlight function. Or if you want it to toggle, I recommend a modification of #sid's answer:
const {useState} = React;
function CardContainer(props) {
const [isSelected, setIsSelected] = useState(false);
<div onClick={() => setIsSelected(!isSelected)} className={isSelected ? "highlighted" : undefined>
}
style.css:
.highlighted {
background-color: 'orange';
}
You can do all of this without any react hook and rely instead on CSS classes. You can use the 'isHighlighted' method to determine if a given component is highlighted or not.

Related

Is there any diference between using ref callback as inline function or as a stable function with useCallback?

I have a component which take a children as props and render it inside a div which has a callback ref.
const ShowMoreText = (props: ShowLessOrMoreProps) => {
const { children } = props;
const [showMore, setShowMore] = useState<boolean>(false);
const [showButton, setShowButton] = useState<boolean>(false);
const [childrenHeight, setChildrenHeight] = useState<number>(9.5);
const measureRef = useCallback((node: HTMLDivElement) => {
if (node) {
setChildrenHeight(node.getBoundingClientRect().height);
}
}, []);
useEffect(() => {
console.log(childrenHeight)// when use measureRef function it gives me 0 when using inline version this gives me correct height and I don't know why
if (childrenHeight > 45) {
setShowButton(true);
}
}, [childrenHeight]);
return (
<Stack
sx={{
alignItems: "flex-start",
}}
>
<Collapse in={showMore} collapsedSize={45}>
<div
// ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
ref={() => measureRef} // if I comment this and use the above commmented version everything works fine
>
{children}
</div>
</Collapse>
{showButton && ( //when using measureRef function this button won't display
<Button onClick={() => setShowMore((prev) => !prev)}>
{showMore ? "Show less" : "Show more"}
</Button>
)}
</Stack>
);
};
the problem is that when I use stable measureRef function the console log inside useeffect prints 0 but in inline ref version everythings works fine. can anyone explain me why ?
ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
You return a function but ref shoulb be an Object (of certain type)
Hence You probably return void function.
if You want to pass (node: HTMLDivElement) => { // setChildrenHeight(node?.getBoundingClientRect().height); // } use unreserved prop then.
props.ref is reserved for useRef() hook .
Finally :
If for some reason Yuu want to save such contruction, do thi that way:
(node: HTMLDivElement) => {
return setChildrenHeight(node?.getBoundingClientRect().height);
}}
Where setChildrenHeight() returns correct object type proper po ref.
I found the solution and share it here in case anyone is curious or have a same problem.
The problem was that the children that I passed to this component was something like this:
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
and because of the delay of request the div height was actually 0 at the beginning. so the actual code for callback ref worked correct and the problem raised because of the api call delay. the solution for me and the mistake that i had been made was not to render the above code conditionally like this:
{dataIsLoading &&
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
}
hopefully this can help someone else.

React render previous values during conditional fade out animation

I am using react-transition-group to fade out various components. I'm converting simple conditional renders such as:
{valueToDisplay && <MyComponent {...valueToDisplay} />}
To transitions such as:
<CSSTransition
in={!!valueToDisplay}
unmountOnExit
classNames="fade"
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
<MyComponent {...valueToDisplay} />
</CSSTransition>
The issue I'm running into is when the "in" property of the transition becomes false, and the exit transition is running, the child component will now have null prop values. This can cause exceptions or cause the child content to flash and change during the exit. What I would like to see instead is that during the exit transition, the content will remain unchanged.
The first solution I came up with was to make child components to cache previous values of their props, and then use those previous values when their props become null. However I don't like this solution because it forces all components which will be transitioned to introduce new and confusing internal logic.
The second attempt I made was to create a wrapper component which cached the previous value of props.children, and whenever "in" becomes false, renders the cached children instead. This essentially "freezes" the children as they were the last time in was true, and they don't change during the exit transition. (If this solution is the general practice, is there a better way of doing this, perhaps with the useMemo hook?)
For such a common use case of fading content out, this solution doesn't seem very intuitive. I can't help but feeling I'm going about this the wrong way. I can't really find any examples of having to cache/memoize content to keep it displaying during fade outs. It seems like something somewhere has to remember the values to display when performing the exit transition. What am I missing?
Here is a minimal example and working example:
import { useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CSSTransition } from 'react-transition-group';
const Pet = ({ type, age }) => {
return (
<div>
Your pet {type || 'null'} is age {age || 'null'}
</div>
);
};
const Fade = ({ show, children }) => {
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
in={show}
unmountOnExit
classNames="fade"
addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
>
<span ref={nodeRef}>
{children}
</span>
</CSSTransition>
);
};
const FadeWithMemo = ({ show, children }) => {
const previousChildren = useRef();
useEffect(() => {
previousChildren.current = show ? children : null;
}, [show, children]);
return (
<Fade show={show}>
{show ? children : previousChildren.current}
</Fade>
);
};
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => setCurrentPet(getPet())}>Set</button>
<button onClick={() => setCurrentPet(null)}>Clear</button>
<div>
The Problem:
<Fade show={!!currentPet}>
<Pet {...currentPet} />
</Fade>
</div>
<div>
Potential Fix:
<FadeWithMemo show={!!currentPet}>
<Pet {...currentPet} />
</FadeWithMemo>
</div>
</>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<Example />);
You can detach the visible condition from the pet state so that you have more granular control over whether something is visible and what is actually being displayed.
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const [showPet, setShowPet] = useState(false);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => {
setCurrentPet(getPet());
setShowPet(true);
}}>Set</button>
<button onClick={() => setShowPet(false)}>Clear</button>
<div>
<Fade show={showPet}>
<Pet {...currentPet} />
</Fade>
</div>
</>
);
};
or you can have the visible be part of the pet state and only set that part to false.

React - How to set className conditionally from a json file

I have the following piece of code for my component. The desired behaviour for the button is to change the className for each li, but this is not working.
const Booking = (props) => {
let { hidden } = useContext(ContextBooking)
let completed = props.completed
return (
<li
className={ //should change according to the button click below
completed && hidden ?
'booking-complete hide'
: completed ?
'booking-complete'
:
'bookings'
}}
key={props.id}
id={props.id}
>
<h3>{props.date}</h3>
<h4>{props.time}</h4>
<h5>{props.name}</h5>
</li>
)
}
{!completed && (
<button
onClick={() => {
if (!completed && !hidden) {
completed = !completed //does make it false
hidden = !hidden //does make it false
} //above works, but won't change classname for each 'li'
else if (completed && hidden) {
completed = !completed
hidden = !hidden
}
}}>
Complete
</button>
)}
In another component, I am creating multiple of these 'Booking' components, by filling in the details with info that come from a json file
const DisplayBookings = () => {
const display = (day) => allBookings.map(item => //allBookings is a json file
item.day === day &&
<Booking
completed={item.completed}
key={item.id}
id={item.id}
time={item.time}
name={item.name}
date={item.date}
/>
)
I emphasised json file as I believe it could be the source of the problem?
A component can in most cases not update its own props, and doing so even if possible is an antipattern.
You can instead use state for updating the components state.
You can create hooks for setting state like this:
const [isCompleted, setIsCompleted] = useState(props.completed);
const [isHidden, setIsHidden] = useState(hidden);
Then in your onClick you use this to update the values:
setIsCompleted(!isCompleted);
setIsHidden(!isHidden);

Change state of individual div (that is inside a map) instead of changing the state of all div's inside the map

I have a component that is checking if some state is true or false. I show a <p> tag if true and hide a <h3>. I am pulling the data from a gaphQL query so there is a map method and there are three <Card>'s now if I click the card and run my showFull function it shows the p tags on all the elements, instead I just want to isolate the specific one it is clicking on.
Here is my component
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<Card onClick={showFull} background={testimonial.testimonialImage.url}>
{testimonialFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
))}
</Testimonials>
Here is my state and my function
const [testimonialFull, setTestimonialFull] = useState(false)
const showFull = () => {
setTestimonialFull(true)
}
Attempting Alexander's answer. The issue I am having now is Cannot read property 'testimonialImage' of undefined
Here is the component
const IndexPage = ({ data }) => {
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}
return (
...
Here is where I invoke it in the map function
...
return (
... (bunch of other jsx/html)
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<TestimonialCard/>
))}
</Testimonials>
...
Wrap the cards in a custom component
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}

Dynamically change styled component based on state AND index

So I have a list that was returned from an API request (not important)
lets call it list = [1,2,3,4,5,6,7];
Now, inside render(), I have something like the following
render(){
<Wrapper>
{list.map((i) => {
return (<Button id = {i} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}
Now, I have another list, lets call it list_check = [false...] (for all 7 elements listed above)
Assume that customFunc changes the respective button id in list_check from false to true.
e.g. if I clicked button 1 (id = 1) then list_check becomes [false, true, false...]
** Now my problem is: I have 2 styled components, Button and Button_Selected,
how can i dynamically change between the 2 styled components so that if that unique button is clicked (change list_check[index] = true), the element becomes Button_Selected instead of Button (The entire list is initalized as Button since all elements are false)
Just to make it clear:
Both arrays are located in this.state and by 2 styled components I mean there exists
const Button = styled.div`
//styling here
`;
and
const Button_Selected = Button.extend`
//Small styling change to differentiate between selected and not selected
`;
edit: all answers are great! too bad I can only select one :(
You could just save the component to another variable.
this.state.list_check.map((item, i) => {
const NecessaryButton = item ? SelectedButton : Button;
return <NecessaryButton onClick={() => this.makeSelected(i)}>Normal Button</NecessaryButton>
})
You can see a live example here.
Although you can return 2 buttons based on conditional rendering .You can also pass props to your styled Button so that based on props you can change your styles.
render(){
<Wrapper>
{list.map((i) => {
return (<Button id = {i} isSelected={this.state.list_check[i]} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}
And in your styled Button:
const Button = styled.div`
styleName: ${props => props.isSelected ? 'styling_if_Selected' : 'styling_if_not_selected'};
`;
The easiest approach would be to have a single Button component and handle in the state if it was selected. Depending on this state you could switch classes.
Example:
class Button extends React.Component {
state = {
isSelected: false
};
handleClick() {
//Here we will set the state change and call the onClick function passed by props
this.setState({isSelected: !this.state.isSelected}, () => { this.props.onClick(); });
}
render() {
return (
<button
class={this.state.isButtonSelected ? "classBtnSelected" : "classBtnDefault"}
onClick={this.handleClick}
/>
);
}
}
Still, if you want to switch components you can use state to control if it was selected and do a conditional rendering. Example:
render(){
<Wrapper>
{list.map((i) => {
return (this.state.isSelected
? <Button id={i} onClick = {this.customFunc.bind(this, i)} />
: <Button_Selected id={i} onClick = {this.customFunc.bind(this, i)} />)
}
</Wrapper>
}

Categories

Resources