I am making a menu and submenus using recursion function and I am in the need of help to open only the respective menu and sub menu's..
For button and collapse Reactstrap has been used..
Recursive function that did menu population:
{this.state.menuItems &&
this.state.menuItems.map((item, index) => {
return (
<div key={item.id}>
<Button onClick={this.toggle.bind(this)}> {item.name} </Button>
<Collapse isOpen={this.state.isToggleOpen}>
{this.buildMenu(item.children)}
</Collapse>
</div>
);
})}
And the buildMenu function as follows,
buildMenu(items) {
return (
<ul>
{items &&
items.map(item => (
<li key={item.id}>
<div>
{this.state.isToggleOpen}
<Button onClick={this.toggle.bind(this)}> {item.name} </Button>
<Collapse isOpen={this.state.isToggleOpen}>
{item.children && item.children.length > 0
? this.buildMenu(item.children)
: null}
</Collapse>
</div>
</li>
))}
</ul>
);
}
There is no problem with the code as of now but I am in the need of help to make menu -> submenu -> submenu step by step open and closing respective levels.
Working example: https://codesandbox.io/s/reactstrap-accordion-9epsp
You can take a look at this example that when you click on any menu the whole level of menus gets opened instead of clicked one..
Requirement
If user clicked on menu One, then the submenu (children)
-> One-One
needs to get opened.
And then if user clicked on One-One,
-> One-One-One
-> One - one - two
-> One - one - three
needs to get opened.
Likewise it is nested so after click on any menu/ children their respective next level needs to get opened.
I am new in react and reactstrap way of design , So any help from expertise would be useful for me to proceed and learn how actually it needs to be done.
Instead of using one large component, consider splitting up your component into smaller once. This way you can add state to each menu item to toggle the underlying menu items.
If you want to reset al underlying menu items to their default closed position you should create a new component instance each time you open up a the underlying buttons. By having <MenuItemContainer key={timesOpened} the MenuItemContainer will be assigned a new key when you "open" the MenuItem. Assigning a new key will create a new component instance rather than updating the existing one.
For a detailed explanation I suggest reading You Probably Don't Need Derived State - Recommendation: Fully uncontrolled component with a key.
const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);
const {Component, Fragment} = React;
const {Button, Collapse} = Reactstrap;
class Menu extends Component {
constructor(props) {
super(props);
this.state = {menuItems: []};
}
render() {
const {menuItems} = this.state;
return <MenuItemContainer menuItems={menuItems} />;
}
componentDidMount() {
loadMenu().then(menuItems => this.setState({menuItems}));
}
}
class MenuItemContainer extends Component {
render() {
const {menuItems} = this.props;
if (!menuItems.length) return null;
return <ul>{menuItems.map(this.renderMenuItem)}</ul>;
}
renderMenuItem(menuItem) {
const {id} = menuItem;
return <li key={id}><MenuItem {...menuItem} /></li>;
}
}
MenuItemContainer.defaultProps = {menuItems: []};
class MenuItem extends Component {
constructor(props) {
super(props);
this.state = {isOpen: false, timesOpened: 0};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}
render() {
const {name, children} = this.props;
const {isOpen, timesOpened} = this.state;
return (
<Fragment>
<Button onClick={isOpen ? this.close : this.open}>{name}</Button>
<Collapse isOpen={isOpen}>
<MenuItemContainer key={timesOpened} menuItems={children} />
</Collapse>
</Fragment>
);
}
open() {
this.setState(({timesOpened}) => ({
isOpen: true,
timesOpened: timesOpened + 1,
}));
}
close() {
this.setState({isOpen: false});
}
}
ReactDOM.render(<Menu />, document.getElementById("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>
<div id="root"></div>
You will want to create an inner component to manage the state at each level.
For example, consider the following functional component (I'll leave it to you to convert to class component):
const MenuButton = ({ name, children }) => {
const [open, setOpen] = useState(false);
const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
return (
<>
<Button onClick={toggle}>{name}</Button>
<Collapse open={open}>{children}</Collapse>
</>
);
};
This component will manage whether to display its children or not. Use it in place of all of your <div><Button/><Collapse/></div> sections, and it will manage the open state for each level.
Keep shared state up at the top, but if you don't need to know whether something is expanded for other logic, keep it localized.
Also, if you do need that info in your parent component, use the predefined object you already have and add an 'open' field to it which defaults to false. Upon clicking, setState on that object to correctly mark the appropriate object to have the parameter of true on open.
Localized state is much cleaner though.
Expanded Example
import React, { Component, useState, useCallback, Fragment } from "react";
import { Collapse, Button } from "reactstrap";
import { loadMenu } from "./service";
const MenuButton = ({ name, children }) => {
const [open, setOpen] = React.useState(false);
const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
return (
<Fragment>
<Button onClick={toggle}>{name}</Button>
<Collapse open={open}>{children}</Collapse>
</Fragment>
);
};
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
currentSelection: "",
menuItems: [],
};
}
componentDidMount() {
loadMenu().then(items => this.setState({ menuItems: items }));
}
buildMenu(items) {
return (
<ul>
{items &&
items.map(item => (
<li key={item.id}>
<MenuButton name={item.name}>
{item.children && item.children.length > 0
? this.buildMenu(item.children)
: null}
</MenuButton>
</li>
))}
</ul>
);
}
render() {
return (
<div>
<h2>Click any of the below option</h2>
{this.state.menuItems &&
this.state.menuItems.map((item, index) => {
return (
<MenuButton name={item.name}>
{this.buildMenu(item.children)}
</MenuButton>
);
})}
</div>
);
}
}
export default Hello;
I have three components:
PageBuilder - is basically a form where the user adds a page name and selects some items.
PageList - stores all pages the user has created in state and renders that state as a list
PageUpdater - takes the form info from PageBuilder and adds it to PageList
When someone clicks a button in PageBuilder, PageBuilder pushes its state to PageUpdater:
// PageBuilder
addNewPage = () => {
let info = {...this.state};
this.props.callBack(info);
}
render(){
return <button onClick={this.addNewPage}>New Page</button>
}
PageUpdater receives state from PageBuilder and passes it down to PageList as props
// PageUpdater
constructor(props){
super(props);
this.state={
data: ''
}
}
updatePageList = (pageAdded) =>{
this.setState({data:pageAdded});
}
render(){
return(
<div>
<PageBuilder callBack={this.updatePageList}/>
<PageList addToList={this.state.data} />
</div>
)
}}
PageList receives the object in props and adds it to its own state. It then returns that state as a list
// PageList
constructor(props){
super(props);
this.state = {pages:''};
}
componentWillReceiveProps(nextProps) {
this.setState({pages: [...this.state.pages, nextProps.addToList]});
}
getPages = () => {
var pages = []
for(var key in this.state.pages){
pages.push(this.state.pages[key].pageTitle)}
return pages; // Return an array with the names
}
render(){
return(
<div>
{this.getPages().map((page, index) => <li key={index}> {page} </li>)}
</div>
)
}}
The problem is that I'm using componentWillReceiveProps to tell PageList when to update its state. So every time the props change, PageUpdaters state is added to the PageList. I only want this to happen when the user clicks the new page button (in the PageBuilder).
One of the main principles of React is that every state is managed by one component, you change and manage your list through three components. Instead your pagelist should be a stateless component:
const PageList = ({ pages }) => (<div>
{ pages.map(({ pageTitle }, index) => <li key={index} >{pageTitle}</li> ) }
</div>);
Now Page should contain the pages in its state and provide a method to change it:
class Page extends React.Component {
state = { pages: [] };
addPage = (page) => {
this.setState(({ pages }) => ({ pages: pages.concat(page) }));
}
render() {
return (<div>
<PageList pages={this.state.pages} />
<PageBuilder add={this.addPage} />
</div>);
}
}
Now the PageBuilder only has to call this.props.add({ pageTitle: "something" }) ...
I am pretty new to react and I have been stuck in a problem for quite a good time.
I have a component DisplayList that iterates through an array of objects and displays them in a list form. Each object becomes a button. I also have another component to render the single view of each item on the list once the item is clicked. My problem is that I get to render the single view of all my items at once INSIDE my displayList component. All I want is to be able to click on the list item and render another component with ONLY info about the item I clicked on and passing my "project" as the props to it. what should I do? What is my error?
My DisplayList component (the part that matters for this problem):
export default class DisplayList extends Component {
constructor() {
super();
this.state = {
displaySingle: false
};
}
handleClick = () => {
this.setState({
displaySingle: true
})
}
render() {
if (this.props.projects && this.props.projects.length > 0) {
return (
<List component="nav">
{this.props.projects.map(project => (
<div className="all-content-wrapper" key={project.id}>
<ListItem button value={project} onClick={this.handleClick}>
{this.state.displaySingle ?
<DisplaySingleItem project={project} /> :
null
}
<ListItemICon>
<img
className="single-item-img-in-list-view"
src={project.img}
/>
</ListItemICon>
You are just a hint away from doing it the right way:
Change the condition in your onClick() as:
onClick={()=>this.handleClick(project.id)}
{ this.state.displayProject_id === project.id ?
<DisplaySingleItem project={project} /> :
null
}
Now define handleClick() as:
handleClick = (project_id) => {
this.setState({
displayProject_id: project_id
})
}
Don't forget to define the initial state in the constructor:
this.state = {
displayProject_id:null
};
<div className="all-content-wrapper" key={project.id}>
<ListItem button value={project} onClick={()=>this.handleClick(project)}>
{this.state.displayProject && this.state.displayProject.id==project.id ?
<DisplaySingleItem project={project} /> :
null
}
<ListItemICon>
<img
className="single-item-img-in-list-view"
src={project.img}
/>
</ListItemICon>
</ListItem>
</div>
change your JSX like the above so you pass the current project to handleClick and change handleClick like the following.
handleClick = (project) => {
this.setState({
displayProject : project
})
}
It should now display the <DisplaySingleItem/> for the clicked project.
For you to be able to show only the project that was selected it is important that you have a reference to it. Right now your handleClick() function does not accept and parameters or data that you can identify the project that was selected.
My solution for you is to pass the project as a parameter to handleClick(project). So your code should look like.
export default class DisplayList extends Component {
constructor() {
super();
this.state = {
displaySingle: false
};
}
handleClick = (project) => {
this.setState({
selectedProject: project, // <- use this state to show your popup or
// whatever view you're using
displaySingle: true
})
}
render() {
if (this.props.projects && this.props.projects.length > 0) {
return (
<List component="nav">
{this.props.projects.map(project => (
<div className="all-content-wrapper" key={project.id}>
<ListItem button value={project} onClick={() => this.handleClick(project)}>
{this.state.displaySingle ?
<DisplaySingleItem project={project} /> :
null
}
<ListItemICon>
<img
className="single-item-img-in-list-view"
src={project.img}
/>
</ListItemICon>
)
}
How can I append a component to a modal using a nested portal?
I am building a page that displays several modals. I want to reuse the same modal but when a button is clicked, the app loads different content into the modal.
WIP CodePen:
https://codepen.io/jtsharpsite/pen/gorvjR
render() {
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}
I have it pretty close to how I think it might work, but I cannot figure out how to append another component to an already appended modal component.
I have a button which calls a handler that opens the modal and specifies the component type.
<button onClick={this.handleShow.bind(this, "p3009", "product")}>
Product 3009
</button>
<button onClick={this.handleShow.bind(this, "s1", "special")}>
Special #1
</button>
The handler is in the App context and opens the modal sibling:
handleShow(modalId, modalType) {
this.setState({ showModal: true });
}
When the modal component mounts, I then try to append the Product when it mounts.
componentDidMount() {
//TODO how to append to parent modal?
modalRoot.appendChild(this.el);
}
How can I move the <Product> or the <Special> up into the <Modal>?
Here's an example of a reusable modal form... Theres a lot here you don't need, but just pay attention to the handleSubmit. Within the handleSubmit we call onComplete() which can be any function we pass through to the reusable modal when we call it, based on what function we want our modal to do... in our case it was capturing username, password, etc.
class ReusableModalForm extends React.Component {
constructor(props){
super(props);
this.state ={
};
}
handleChange(e) {
let {name, value} = e.target;
this.setState({
[name]: value,
usernameError: name === 'username' && !value ? 'username must have a value' : null,
emailError: name === 'email' && !value ? 'email must have a value' : null,
passwordError: name === 'password' && !value ? 'password must have a value' : null,
});
}
handleSubmit(e) {
e.preventDefault();
this.props.onComplete(this.state)
}
render() {
return (
<Modal
open={this.state.createAccountModalOpen}
trigger={<Link size="m" theme="bare" href="#" className="main-menu-item" onClick={this.handleSubmit}>{this.props.buttonText}</Link>}
closeIcon
onClose={() => { this.setState({
createAccountModalOpen: false,
}); }}
>
<Header icon='add user' content='Create account' />
<Modal.Content>
<Form />
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.handleSubmit}>
<Icon name='add user' /> {this.props.buttonText}
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default ReusableModalForm;
Based on this logic, you could construct a modal with a series of "trigger texts" to only render certain things based on what kind of props are passed down when you use your reusable modal.
Example.
<ReusableModal triggerText={'showAdress'} onComplete={this.showUsersHomeOnGoogle} />
Then in your reusableModal somewhere....
{this.props.triggerText === showAdress ? this.setState=({showHomeAdressPortion: true)}
UPDATES
class App extends React.Component {
constructor(props) {
super(props);
this.state = { showModal: false };
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow(modalId, modalType) {
console.log(
"Append content with ID: " + modalId + " of component type: " + modalType
);
//Here you should do a "fetch" first and grab your entire product information based on chosen ID and store it to state as an object...
//setState({activePortal: someContentObject})
//Then... I would use promise or aync await, show modal...
//Have Modal display from this state object
this.setState({ showModal: true, activePortal: });
//if it is a product
if (modalType == "product") {
//fetch additional images and add to state
//add images to {product.additionalPics}
}
//TODO re-render the modal with the appending element portal
}
handleHide() {
this.setState({ showModal: false });
}
render() {
//console.log("RENDER App");
// Show a Modal on click.
const modal = this.state.showModal ? (
<Modal>
<div className="modal">
<div className="modal-content-wrapper">
{/* I think what you're asking to do here is display basically "any" information that comes from a product fetch. This isnt possible without templeting somehow.. You will have to hard code like "title" "body" "image" into this modal...
<h1>this.state.activeProduct.title</h1>*/}
{/* CONTENT NEEDS TO GO HERE SOMEHOW */}
{this.props.children}
</div>
<button onClick={this.handleHide}>Hide modal</button>
</div>
</Modal>
) : null;
Have you tried using a parent-child pattern in the portal?
For example, make your children like this and just have ModalDisplay render its children. I don't know if what you are trying to do is possible because I think it requires portals to support fragments and I'm not sure if they do.
I don't know if I got the context of your question right but the idea is the same you have a single child that itself has children instead of having more than 1 child in the portal call.
<ModalDisplay>
<Special title="special" text="text" />
<Product title="product" pic="picture" />
</ModalDisplay>
I have two components Class and Students. The Class component renders and returns a list of Classes in <li>. I want to add click events to display the Students for each Class in Class component.
I have the following in the render method of the Class component:
render(){
const renderClasses = () =>
this.props.classes.map(class => {
return (
<li>
{class.name}
//class object also has a property of 'students'
</li>
)
})
return(
<div>
{ renderClasses() }
</div>
)
}
I want to be able to click on the anchor tags and display the corresponding students for that class. Of course the Student component should receive a prop as follows:
<Students students={this.class.students} />
Thanks in advance!
You can keep a component state to save the class index that should show its students, and then add an onClick handler on the anchor to change that index.
Try the code below:
export default class Test extends Component {
constructor(props)
{
super(props);
this.state = {activeClassIndex : -1}
this.setActiveClassIndex = this.setActiveClassIndex.bind(this);
}
setActiveClassIndex(index){
this.setState({
activeClassIndex : index
})
}
render(){
const renderClasses = () =>
this.props.classes.map( ( currentClass , index ) => {
return (
<li>
<a href="#" onClick={ () => { this.setActiveClassIndex(index) } }>{currentClass.name}</a>
{this.state.activeClassIndex == index ? <Students students={currentClass.students} /> : "" }
</li>
)
})
return(
<div>
{ renderClasses() }
</div>
)
}
}