React: using handleclick to toggle class of mapped element by key - reactjs

I'm going through the React documentation and trying to change the condition of one element within a map based on the click of a corresponding element (associated with the same key). Using a handleClick that looks something like this:
handleClick: function(e) {
this.setState({condition: !this.state.condition});
console.log('clicked' + e);
}
I have some menu items drawn this way:
return (
<li key={i}>
<a
onClick={_this.handleClick.bind(_this, i)}
data-nav={'nav-' + el.label.en.toLowerCase()}>
{el.label.en}
</a>
</li>
);
And some submenu sections whose class I want to toggle based on click of the above:
return (
<section
className={_this.state.condition ? "active" :""}
key={i}
id={'nav-' + el.label.en.toLowerCase()}>
<ul>
<GetChildren data={el.children} />
</ul>
</section>
);
Right now, obviously, when I click the first, all elements toggle their class. For the life of me, I can't figure out how to pass the key, so that if I'm clicking on item 1, the section with key {1} gets an "active" class, and others do not. In javascript with jquery I could get at it using the data-attribute. I'm sure react has a simple way that I'm just not understanding.

You can do this by setting the active key in your state object instead of toggling a condition.
So in your handleClick
this.setState({
activeKey: e
});
then in your <section>
className={_this.state.activeKey === i ? "active" : ""}

Related

Why is useState and DOM not recreating/identifying/re-rendering the new state even with useState([...Array])?

I am trying to render and then re-render using array.map() an array of objects. The useEffect is called whenever the state(selected) changes for the options ('All','BACKEND','FRONTEND','ML').
The
All Selected rendering all data
Backend Selected showing "backendData"
Frontend Selected etc etc
MY ISSUE:
The rendering of the data is fine but I have added a basic animation which fades the elements in one by one using "react-awesome-reveal" library. I did the same with plain CSS so I don't think my error lies here.
The animation works if you refresh or direct towards the page or THE NUMBER OF ELEMENTS TO BE RE-RENDERED IS MORE THAN THE ELELEMNTS ON SCREEN THE LATER ELEMENTS WILL HAVE THE ANIMATION.
EG: If I go from "Frontend" to "Backend" then there are currently 2 elements in the DOM but now 3 need to be re-rendered so the first 2 elements do not carry the animation but the third does. It also works in the reverse where if I go from "All" 9 elements to "ML" 2 elements the elements are automatically rendered without the animation.
It seems like the DOM is looking at what is already rendered and deciding to not re-render the elements that are already there thus the FADE animation is not working.
Ideally I want the setData(...) to create a new state so that the array is mapped as if it the page was refreshed.
Here is the code snippet:
import {
backendCourses,
frontendCourses,
mlCourses,
allCourses,
} from "../../data";
export default function Courses() {
const [selected, setSelected] = useState();
const [data, setData] = useState(allCourses);
const list = [
{ id: "all", title: "All" },
{ ....},
...
];
useEffect(() => {
switch (selected) {
case "all":
setData(Array.from(allCourses));
break;
case "backend":
setData([...backendCourses]);
case ....
}, [selected]);
return (
<div className="courses" id="courses">
<h1>Courses</h1>
<ul>
{list.map((item, index) => (
<CoursesList
...
active={selected === item.id}
setSelected={setSelected}
/>
))}
</ul>
<div className="container">
{data.map((item, index) => (
<Fade ...>
<div ...>
<img ...>
</div>
</Fade>
))}
</div>
</div>
);
}
//CoursesList Component
export default function CoursesList({ id, title, active, setSelected }) {
return (
<li
className={active ? "coursesList active" : "coursesList"}
onClick={() => setSelected(id)}
>
{title}
</li>
);
}
The mapping of the elements using Fade from "react-awesome-reveal" FULL-CODE.
<div className="container">
{data.map((item, index) => (
<Fade delay={index * 500}>
<div className="item">
<img src={item.img} alt={item.title} id={index}></img>
<a href={item.url} target="_blank">
<h3>{item.title}</h3>
</a>
</div>
</Fade>
))}
Here is the full useEffect hook code. I initially thought I was not creating a new array only referencing it but have tried multiple ways to clone the array without success. The courses data for now is hard-coded and just read from a file.
useEffect(() => {
switch (selected) {
case "all":
setData(Array.from(allCourses));
break;
case "backend":
setData([...backendCourses]);
break;
case "frontend":
setData([...frontendCourses]);
break;
case "ml":
setData([...mlCourses]);
break;
default:
setData([...allCourses]);
}
}, [selected]);
Sorry for the long post but trying to be as detailed as I can.
It seems like the DOM is looking at what is already rendered and deciding to not re-render the elements that are already there thus the FADE animation is not working.
It's React that does that, not the DOM. That's part of its job: To avoid recreating DOM elements that already have the content you're trying to show.
If you want React to think you're providing new elements every time, even when in fact they're the same, give the elements a new key. At the moment, your map call isn't giving them a key at all (which it should be), so React defaults to using the element's position in the list as a key.
Instead, provide a key on the Fade component and make sure it changes when selected changes, probably by using selected itself in the key:
return (
<div className="container">
{data.map((item, index) => (
// I'm assuming `item.url` is unique within the array, adjust this
// if not. (*Don't* use `index`, indexes make poor keys, see the
// link above.
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
<Fade key={selected + "::" + item.url} delay={index * 500}>
<div className="item">
<img src={item.img} alt={item.title} id={index}></img>
<a href={item.url} target="_blank">
<h3>{item.title}</h3>
</a>
</div>
</Fade>
))}
</div>
);
That way, the elements get reused correctly when the component is rendered for other reasons (because each of them has a stable key), but when you change selected, they all get replaced with new elements because the key changes.

Target specific child inside React loop with refs and state onClick

I've got a mobile nav menu with a couple dropdown menus:
/* nav structure */
/about
/portfolio
/services
/service-1
/service-2
contact
etc..
I'm creating this menu dynamically with a loop in my render function.
I've set up a state variable to assign a class to the active dropdown menu. I'm using an onClick event/attribute on the trigger element (a small arrow image) to apply an active class to the respective dropdown menu. Kinda...
const myNavMenu = () => {
const [isSubMenuOpen, toggleSubMenu] = useState(false)
return (
<nav id="mainNav">
<ul>
{navItems.items.map((item, i) => (
<li key={i} className={
(item.child_items) !== null ? 'nav-item has-child-items' : 'nav-item'}>
<Link to={item.slug}>{item.title}</Link>
{(item.child_items === null) ? null :
<>
<img className="arrow down" src={arrowDownImg} onClick={() => toggleSubMenu(!isSubMenuOpen)} />
{printSubMenus(item)}
</>
}
</li>
))}
</ul>
</nav>
)
}
/**
* Prints nav item children two levels deep
* #param {Obj} item a navigation item that has sub items
*/
function printSubMenus(item) {
return (
<ul className={(isSubMenuOpen.current) ? "sub-menu sub-menu-open" : "sub-menu"}>
{item.child_items.map( (childItem, i) => (
<li className="nav-item" key={i}>
<Link to={childItem.slug}>{childItem.title}</Link>
{(childItem.child_items === null) ? null :
<>
<ul className="sub-sub-menu">
{childItem.child_items.map( (subItem, i) => (
<li className="sub-nav-item" key={i}>
<img className="arrow right" src={arrowRight} alt={''} />
<Link to={subItem.slug}>{subItem.title}</Link>
</li>
))}
</ul>
</>
}
</li>
))}
</ul>
)
}
}
*<Link> is a Gatsby helper component that replaces <a> tags.
The issue is that when I click my trigger, the active class is being applied to both (all) sub-menus.
I need to insert some sort of index (or Ref) on each trigger and connect it to their respective dropdowns but I'm not quite sure how to implement this.
I was reading up on useRef() for use inside of function components. I believe that's the tool I need but I'm not sure how to implement it in a loop scenario. I haven't been able to find a good example online yet.
Thanks,
p.s. my functions and loops are pretty convoluted. Very open to refactoring suggestions!
Move sub-menu to a new react component and put all the related logic in there (applying class included).

How to deal with multiple <select> dropdown menus in the same class component that use the same state to pass a value to redux?

This code works fine if the user selects something from each dropdown menu, but if they forget to make a selection, it will just use the value selected from the previous dropdown menu. Also if they don't make any selection at all and submit, it will obviously submit the default value stored in the state which is "0".
Anyone happen to have a workaround for this? Thanks.
export class Content extends Component {
constructor(props){
super(props)
this.state = {
selectedOption: 0
}
}
handleOptionChange = e => {
this.setState({
selectedOption: e.target.value
})
}
handleSubmit = e => {
e.preventDefault()
}
render() {
let snowboardItems = this.props.snowboards.map((board,index) => {
return <div><form onSubmit={this.handleSubmit}>
<li key={index} className="list_item">
<div className="content_div1">
<h3>{board.name}</h3>
<h3>$ {board.price}</h3>
<h4>{board.terrain}</h4>
<h4>Shape: {board.shape}</h4>
<p>Board Length:</p>
<select value={this.state.selectedOption} onChange={this.handleOptionChange}>
{board.length.map((item, index) =>
<option value={item} key={index}>{item}</option>
)}
</select> cm
</div>
<div className="content_div2">
<button className="content_button" type="submit" onClick={() => this.props.addToCart({board}, this.state.selectedOption)}>Add to Cart</button>
<img className="image" src={board.imageurl} />
</div>
</li>
</form>
</div>
})
This is really a case where you should separate this into two components: one to render the list of items (you could do this in the parent passing the props too), and another to render the item and possibly handle its state.
If for some reason you can't though, you'll probably want to separate each board option into its own property on state. Here's an example where state is updated dynamically:
https://codesandbox.io/embed/snowboards-pl9r5
You should always code defensively, so in the example there's a "short circuit" check to make sure that a length was selected before adding it to the cart. Also the select field is marked as required so that you can use HTML5 as another fallback validator.
You can check it by trying to add an item without a length and also selecting different options and adding them to the cart (logging them in the console).
On another note: I changed it to more specific keys because mapping multiple lists and using the index as a key will result in duplicate keys. Keys are how react knows which item is which, and you don't want to confuse react!
P.S. Way to bum me out giving a snowboard example in the summer! lol Happy hackin'

How to apply a CSS style in React based on condition and incorporate it with another class?

I am modifying the value of a property for certain list items in an array, depending on whether or not a checkbox is checked/unchecked. The list items are named todo and the property where I am storing whether or not the checkbox is checked/unchecked (T/F) on a given todo, is called completed
Given this handler, where I am adjusting the value of an individual todo's completed property when a check box is checked/unchecked:
handleCompletedTodo(todo) {
var completedTodoId = todo.id;
var found = this.state.todos.find(todosArray=>todosArray.id === completedTodoId);
if(found) found.completed = !found.completed;
}
How can I now set a CSS conditional style to it's text (named: "strike-through") in this stateless component, to indicate that a todo's item's completed value is T/F? Please note there is already a class that is present called "list-group-item", which will have to be incorporated with the conditional style:
<ul className = "list-group">
{props.todosArray.map(todo => {
return <li className='list-group-item' key ={todo.id}><input type="checkbox" onChange= {() => props.onDone(todo)}/>{todo.text}<a onClick={() => props.onEdit(todo)}
className="edit-todo glyphicon glyphicon-edit d-flex justify-content-between align-items-center" href="#"></a><a onClick={() => props.onDelete(todo)}
className="delete-todo glyphicon glyphicon-trash" href="#"></a></li>
})
}
</ul>
Thank you for your help.
You can use a ternary operator to and ES6's string interpolation using backticks inside of classname.
<div className={`some other classes${completed ? " strike-through" : ""}`} />

How does the ReactBootstrap OverlayTrigger container property work?

I have a popover inside OverlayTrigger.
I define it as
const myOverlayTrigger = <ReactBootstrap.OverlayTrigger
placement='bottom' overlay={<ReactBootstrap.Tooltip>...</ReactBootstrap.Tooltip>}>
{myContent}
</ReactBootstrap.OverlayTrigger>;
Then I render it inside one of my elements like that:
<li>{myOverlayTrigger}</li>
I want to render OverlayTrigger itself inside <li> but it renders inside body, as defined in documentation. I'm trying to use container attribute to render it inside parent <li>.
First, I tried to assign ID to <li> and pass this ID as a string to container=... (which isn't a best way).
Second, I tried to create additional element <span></span> and render it inside along with {myOverlayTrigger}. Also I pass it (assigned to variable) to container attribute
const c = <span></span>;
... container={c} ...
<li>{c} {myOverlayTrigger}</li>
Both approaches consistently gives an error not a dom element or react component.
Obviously assigning <li>...</li> itself as a container doesn't work either as it being defined after myOverlayTrigger is defined.
Question: how to use it right?
ReactBootstrap.Overlay is recommended for the reason listed in the document.
The OverlayTrigger component is great for most use cases, but as a
higher level abstraction it can lack the flexibility needed to build
more nuanced or custom behaviors into your Overlay components. For
these cases it can be helpful to forgo the trigger and use the Overlay
component directly.
For your case, the code below renders the ReactBootstrap.Overlay component into a list item with React ref attribute.
getInitialState() {
return (
show: false
);
},
render() {
return (
<ul>
<li ref="dest" onClick={ () => {
this.setState( { show: !this.state.show } );
}}>my contents</li>
<ReactBootstrap.Overlay placement="bottom"
show={ this.state.show } container={ this.refs.dest }>
<ReactBootstrap.Tooltip>tooltip</ReactBootstrap.Tooltip>
</ReactBootstrap.Overlay>
</ul>
);
}
When the tooltip is displayed by clicking, the resulting HTML would be
<ul data-reactid=".0.1.0.0.0.0.0.1.1.1.1:$3.1.1">
<li data-reactid=".0.1.0.0.0.0.0.1.1.1.1:$3.1.1.0">
contents
<div>
<div role="tooltip" class="fade in tooltip right" data-reactid=".3">
<div class="tooltip-arrow" data-reactid=".3.0"></div>
<div class="tooltip-inner" data-reactid=".3.1">My tooltip</div>
</div>
</div>
</li>
<span data-reactid=".0.1.0.0.0.0.0.1.1.1.1:$3.1.1.1">,</span>
<noscript data-reactid=".0.1.0.0.0.0.0.1.1.1.1:$3.1.1.2"></noscript>
</ul>

Resources