React + MaterializeCSS + dynamic rendering dropdowns (fault position) - reactjs

I am using ReactJS with JSX and materializeCss for styling.
I am facing this problem for a couple of days now...
In short what I have:
class PreventiveCollection extends Component {
componentDidUpdate = () => updateDropdowns(); // which stands for initializing materialize dropdowns
render() {
const myList = this.props.data.map( (i ,index) => (
<li key={index} className="collection-item">
<i data-target={i.id} className="dropdown-trigger material-icons right">more_vert</i>
<ul id={i.id} className='dropdown-content'>
<li><a>option 1</a></li>
<li><a>option 2</li>
</ul>
</li>
));
return <div>{myList}</div>
}
}
I am rendering a collection of items, where each item has a dropdown menu. The problem is that when I click "more_vert" button, the dropdown opens on the top of the component, right below the first menu button.
They all work and are connected with each item correctly. The problem is only the position. Please see attached screen
https://i.stack.imgur.com/PFfV4.png
On the left screen, the top menu-button was clicked, it's all fine.
On the right screen, the 3rd item menu-button was clicked. The dropdown content is fine, but the position is wrong. it always sticks to this first menu button...
I have tried:
move the menu to separate component with its own update state functions
moved updateDropdowns() from componentDidUpdate to the end of the
render() method.
wrap menus objects with divs with mix of position relative/absolute
none of the above worked.

The problem was related to MaterialCss and the first relative parent. The solution is to wrap full menu code with div with position relative, like so:
class PreventiveCollection extends Component {
componentDidUpdate = () => updateDropdowns(); // which stands for initializing materialize dropdowns
render() {
const myList = this.props.data.map( (i ,index) => (
<li key={index} className="collection-item">
<div style={{position: 'relative'}}>
<i data-target={i.id} className="dropdown-trigger material-icons right">more_vert</i>
<ul id={i.id} className='dropdown-content'>
<li><a>option 1</a></li>
<li><a>option 2</li>
</ul>
</div>
</li>
));
return <div>{myList}</div>
}
}

Related

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).

Prevent Component/PureComponent with styled-components from re-rendering all items after state change

After adding styled-components to this example, we've noticed that our list component updates everything when only one item in state is modified.
The list rendering highlights (from React dev-tools) are excessive when adding/removing a single entry. One item is removed/added, then all items get highlighted.
code samples
github: https://github.com/chancesmith/reactjs-optimize-list-updates/tree/master/src
codesandbox: https://codesandbox.io/s/wn45qw44z5
Here is an example of the right list component (CategorizedList.js)
import styled from "styled-components";
const Item = styled.li`
color: #444;
`;
class CategorizedList extends PureComponent {
render() {
return (
<div>
{this.props.categories.map(category => (
<ul>
<li key={this.props.catStrings[category]}>
{this.props.catStrings[category]}
</li>
<ul>
{this.props.items.map((item, index) =>
item.category === category ? (
<div key={item.label + index}>
<Item>{item.label}</Item>
</div>
) : null
)}
</ul>
</ul>
))}
</div>
);
}
}
Preference
I'd prefer to use PureComponent so that shouldComponentUpdate() gets handled automatically.
Question
How can we make sure only the modified objects in items state are re-rendered?
If the data changes , the view will re-render. It shouldn't be an expensive process since it happens once on add/remove action. If you find performance issues it might be caused from something else.
In general this would be the way you can have some control on pure-components re-render:
https://reactjs.org/docs/react-api.html#reactmemo

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

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" : ""}

How to pass a method to a child react component?

folks!
I'm starting to construct an app with webpack, react, redux, etc. And im not sure if im facing the situation as i should:
I want to develop a single page app, having a single react node "<myApp/>" that will contain everything inside. The first thing that will appear on my page, is a bootstrap navbar. What i want, is to create a react component that renders the navbar and to pass him 1) a list of strings, and 2) a function that should be called when one of that items is clicked. I want to do it in that way, because i would like to track at top level the "page" where i am (it will be always one of the navbar items).
Here my myApp component:
export default class myApp extends React.Component {
constructor(props) {
super(props);
this.handleNavbarClick = ::this.handleNavbarClick;
this.state = {
currentPage: 'Home',
navbarItems : ['Home', 'contact', 'etc']
};
}
handleNavbarClick(e) {
//debug alert:
alert(e);
this.state.currentPage = e;
}
render() {
return (
<div>
<NavBar items={this.state.navbarItems} itemClickHandler={this.handleNavbarClick}/>
<Wellcome />
<LandingMenu />
CurrentPage: {this.state.currentPage}
</div>
);
}
}
and here my navbar component:
export default class NavBar extends React.Component {
constructor(props) {
super(props);
}
render() {
var handler = this.props.itemClickHandler;
var itemsHtml = this.props.items.map(function(i) {
return <li className="nav-item"> <a className="nav-link" onClick={handler(i)}>{i}</a></li>
});
return (
<nav className="navbar navbar-static-top navbar-dark bg-inverse">
<a className="navbar-brand" href="#"><img src={LogoSrhSmall} /></a>
<ul className="nav navbar-nav">
{itemsHtml}
</ul>
</nav>
)
}
}
As it is, when i click one of the navbar items, nothing happens, but when the page loads, the alert of the function handleNavbarClick is automatically called for each item, and i have no idea why :(
Can somebody please tell me what im doing wrong? im facing the problem in the right way?
Thanks in advance,
John
#janaka-stevens, #Jamby, Thank you!
Applying some changes from your answers, i was able to get the name out by storing it on the attribute "id" and accessing it throw e.target.id.
For this case that's fine, but supose that instead of work with simple strings, i would like to work with an array of more complex objects, let's say:
[
{ name: 'home', url: '/someroute', importantData: {...} },
{ name: 'otherLink', url: '/someOtherroute' , importantData: {...} }
]
and i would like to pass the complete selected object to the parent, myApp?
At the moment, it looks like that:
on myApp:
...
handleNavbarClick(e) {
this.setState({currentPage : e.target.id}) ;
}
...
and on the NavBar:
...
renderNavItem(item) {
return (<li className="nav-item">
<a id={item} className="nav-link" onClick={this.props.itemClickHandler}>{item}</a>
</li>)
}
render() {
return (
<nav className="navbar navbar-static-top navbar-dark bg-inverse">
<a className="navbar-brand" href="#"><img src={LogoSrhSmall} /></a>
<ul className="nav navbar-nav">
{this.props.items.map(this.renderNavItem, this)}
</ul>
</nav>
)
}
...
but if on the <a> tag, instead of to use:
onClick={this.props.itemClickHandler}
i use:
onClick={this.props.itemClickHandler(item)}
the itemClickHandler is called on each render iteration, as react would "call" the method when iterating through the items to display them (without any user interaction).... any idea why?
Thanks in advance,
John
I'm going to suggest some changes to your code to make it more readible:
For the itemClickHandler you use something other than (e) because e usually means event. For that you should do itemClickHandler(page).
And then inside the Navbar component, you probably want to do something like this:
export default class NavBar extends React.Component {
constructor(props) {
super(props);
}
renderNavItem(item) {
<li className="nav-item">
<a className="nav-link" onClick={this.props.handleNavbarClick(item)}>{item}</a>
</li>
}
render() {
return (
<nav className="navbar navbar-static-top navbar-dark bg-inverse">
<a className="navbar-brand" href="#"><img src={LogoSrhSmall} /></a>
<ul className="nav navbar-nav">
{this.props.items.map(this.renderNavItem)}
</ul>
</nav>
)
}
}
BUT the main thing you're doing wrong is that you're NEVER setting state.
The #1 rule in React is you never want to alter a prop or a state (by doing this.prop.whatever = blar or this.state.whatever = blar. If you do that, then nothing will change and components you're passing props or state to may not have the correct information. So if you want to change state you must call this.setState({currentPage: page}).
So inside your handleNavbarClick you want to change it to:
handleNavbarClick(page) {
//debug alert:
alert(page);
this.setState({currentPage: page});
}
This will tell React that you've changed state and wish to re-render.
You have two problems. The first is the e in handleNavbarClick(e) is event. What you would want to do is have an identifier for each of your items list. Then you would get e.target.id. Of course you will also need to add the id in your mapped list. The second problem is you need to pass 'this' to your mapped list like so;
render() {
var itemsHtml = this.props.items.map(function(i) {
return <li className="nav-item"> <a id={i.id} className="nav-link" onClick={this.props.itemClickHandler}>{i}</a></li>
}, this);
Note the second parameter in map.
I had the problem where the handlers ran by themselves without actually clicking the buttons. I was calling the handler with an argument like you were are first. I removed that, and the behavior stopped. I tried the bind(null, props) trick and it worked for the props, but I didn't know how to access the event itself in the handler so I could e.preventDefault().
After looking for a minute, I found this gem, which iterates through all the unnamed arguments a function has received.
for (var i=0; i<arguments.length; i++) {
console.log(arguments[i])
};
Using this, I could access the event in the handler with
e = arguments[1]
and now all is well.
Thanks for giving me a starting point!

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