Open a <Modal> by clicking on an element rendered in another component - reactjs

I am using a group of Semantic UI <Item> components to list a bunch of products. I want to be able to edit the the details of a product when the <Item> is clicked, and I thought the best way to achieve this would be using a <Modal> component.
I want to have everything split into reusable components where possible.
(Note: I've purposefully left out some of the import statements to keep things easy to read.)
App.js
import { ProductList } from 'components';
const App = () => (
<Segment>
<Item.Group divided>
<ProductList/>
</Item.Group>
</Segment>
)
export default App;
components/ProductList.js
import { ProductListItem } from '../ProductListItem';
export default class ProductList extends Component {
constructor() {
super()
this.state = { contents: [] }
}
componentDidMount() {
var myRequest = new Request('http://localhost:3000/contents.json');
let contents = [];
fetch(myRequest)
.then(response => response.json())
.then(data => {
this.setState({ contents: data.contents });
});
this.setState({ contents: contents });
}
render() {
return (
this.state.contents.map(content => {
return (
<ProductListItem
prod_id={content.prod_id}
prod_description={content.prod_description}
category_description={content.category_description}
/>
);
})
)
}
}
components/ProductListItem.js
export default class ProductListItem extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Item key={`product-${this.props.prod_id}`} as='a'>
<Item.Content>
<Item.Header>{this.props.prod_description}</Item.Header>
<Item.Description>
<p>{this.props.prod_description}</p>
</Item.Description>
</Item.Content>
</Item>
)
}
}
All of this works nicely and the list of products displays as it should.
I've also created a basic modal component using one of the examples in the Modal docs:
components/ModalExampleControlled.js
export default class ModalExampleControlled extends Component {
state = { modalOpen: false }
handleOpen = () => this.setState({ modalOpen: true })
handleClose = () => this.setState({ modalOpen: false })
render() {
return (
<Modal
trigger={<Button onClick={this.handleOpen}>Show Modal</Button>}
open={this.state.modalOpen}
onClose={this.handleClose}
size='small'
>
<Header icon='browser' content='Cookies policy' />
<Modal.Content>
<h3>This website uses cookies etc ...</h3>
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.handleClose}>Got it</Button>
</Modal.Actions>
</Modal>
)
}
}
So this will create a button that reads Got it wherever <ModalExampleControlled /> is rendered, and the button causes the modal to appear - great.
How do I instead get the modal to appear when one of the <Item> elements in the product list is clicked (thus getting rid of the button)?
Thanks so much for your time.
Chris

Your problem is that currently the modal manages its own state internally. As long as this is the case and no other component has access to that state, you can not trigger the modal component from outside.
There are various ways to solve this. The best way depends on how your app is set up. It sounds like the best way to go is to replace the internal modal state with a prop that is passed to the modal from a higher order component that also passes open/close functions to the relevant children:
// Modal.js
export default class ModalExampleControlled extends Component {
render() {
return (
{ this.props.open ?
<Modal
open={this.props.open}
onClose={this.props.handleClose}
size='small'
>
<Header icon='browser' content='Cookies policy' />
<Modal.Content>
<h3>This website uses cookies etc ...</h3>
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.props.handleClose}>Got it</Button>
</Modal.Actions>
</Modal>
: null }
)
}
}
// App.js
import { ProductList } from 'components';
class App extends Component {
handleOpen = () => this.setState({ open: true })
handleClose = () => this.setState({ open: false })
render(){
return(
<Segment>
<Item.Group divided>
<ProductList/>
</Item.Group>
<Modal open={this.state.open} closeModal={() => this.handleClose()}}
</Segment>
)
}
}
export default App;
Keep in mind that this code is rather exemplary and not finished. The basic idea is: You need to give control to the highest parent component that is above all other components that need access to it. This way you can pass the open/close functions to the children where needed and control the modal state.
This can get unwieldy if there is a lot of this passing. If your app gets very complex it will become a matter of state management. When there is a lot going on a pattern like Redux might help to manage changing states (e.g. modals) from everywhere. In your case this might be finde, though.

Related

Why I can't use an imported component inside a functional component in React?

I am new to React. For the code readability, instead of in-line styled button, I want to write it as a separate class component. I created a customed button 'addImageButton'and imported it to another .js file. It doesn't render the customer button when I try to use it within a functional component. How can I make the functional component be able to use the imported button? Thanks!
//addImageButton.js
import React, { Component } from "react";
class addImageButton extends Component {
render() {
return (
<div>
<button
style={{
borderStyle: "dotted",
borderRadius: 1,
}}
>
<span>Add Image</span>
<span>Optional</span>
</button>
</div>
);
}
}
export default addImageButton;
//AddNewTaskButton.js
import React, { Component } from "react";
import Modal from "react-modal";
**import addImageButton from "../addImageButton";**
class AddNewTaskButton extends Component {
constructor(props) {
super(props);
this.state = {
show: false,
};
this.setShow = this.setShow.bind(this);
this.closeShow = this.closeShow.bind(this);
this.addTaskModal = this.addTaskModal.bind(this);
}
setShow() {
this.setState({
show: true,
});
}
closeShow() {
this.setState({
show: false,
});
}
addTaskModal = () => {
return (
<div>
<Modal
isOpen={this.state.show}
onRequestClose={() => this.closeShow()}
>
**<addImageButton />**
</Modal>
</div>
);
};
render() {
return (
<div>
<button onClick={() => this.setShow()}>
<img src={addIcon} alt={text}></img>;
<span>text</span>
</button>
<this.addTaskModal className="modal" />
</div>
);
}
}
export default AddNewTaskButton;
Easier way would be to just use functional components. Also, react components should be upper case, like so:
export default function AddImageButton() {
return (
<div>...</div>
)
}
create a different component for Modal
import Modal from './Modal'
import AddImageButton from './AddImageButton'
function AddTaskModal() {
return (
<div>
<Modal> <AddImageButton/> </Modal>
</div>
)
}
then
import AddTaskModal from './AddTaskModal'
function AddNewTaskButton() {
return (
<div>
<AddTaskModal/>
</div>
)
}
I don't know your file directories, so I just put randomly.
as for your question, try to make the AddImageButton as a class and see if it renders then. If it doesn't it might be due to something else. Do you get errors? Also maybe create the AddTaskModal class separately and render it out as a component. Maybe that'll help

React - loop: pass data to Modal component

I am quite new to React and I am playing around with Gatsby.
I would like to show a list of team members and when clicking on one, further details should show in a modal (react-modal). My data is gathered after which I loop through it and create a modal (based on https://codepen.io/claydiffrient/pen/pNXgqQ) for each member. However, I cannot figure out how to pass all data to the modal.
What I have now (simplified):
import React from "react";
import Modal from "react-modal";
export default class Team extends React.Component {
constructor() {
super();
this.state = {
showModal: false,
};
this.handleOpenModal = this.handleOpenModal.bind(this);
this.handleCloseModal = this.handleCloseModal.bind(this);
}
handleOpenModal = (member) => {
this.setState({
showModal: true,
memberDetail: member,
});
};
handleCloseModal() {
this.setState({ showModal: false });
}
render() {
return (
<>
{this.props.data &&
this.props.data.map(({ node: member }) => (
<div key={member.frontmatter.title}>
<h3
dangerouslySetInnerHTML={{
__html: member.frontmatter.title,
}}
></h3>
<button
onClick={() =>
this.handleOpenModal(`${member.frontmatter.title}`)
}
>
Details
</button>
</div>
))}
<Modal
isOpen={this.state.showModal}
contentLabel="Modal"
onRequestClose={this.handleCloseModal}
>
{this.state.memberDetail}
<button onClick={this.handleCloseModal}>Close Modal</button>
</Modal>
</>
);
}
}
This seems to work fine. However, now I would like to make all member data available to the Modal. And this I cannot figure out. The data comes from markdown files and looks like follows:
---
title: Some title
subTitle: some subtitle
mainImage: /images/team/hello.jpg
sequence: 20
---
Some markdown text here.
Could be related to How can I pass data to Modal using react. Edit or Delete? but I am not sure (cannot get it to work with the information from there).

react recreating a component when I don't want to

I'm super new to react, this is probably a terrible question but I'm unable to google the answer correctly.
I have a component (CogSelector) that renders the following
import React from "react"
import PropTypes from "prop-types"
import Collapsible from 'react-collapsible'
import Cog from './cog.js'
const autoBind = require("auto-bind")
import isResultOk from "./is-result-ok.js"
class CogSelector extends React.Component {
constructor(props) {
super(props)
this.state = {
docs: null,
loaded: false,
error: null
}
autoBind(this)
}
static get propTypes() {
return {
selectCog: PropTypes.func
}
}
shouldComponentUpdate(nextProps, nextState){
if (nextState.loaded === this.state.loaded){
return false;
} else {
return true;
}
}
componentDidMount() {
fetch("/api/docs")
.then(isResultOk)
.then(res => res.json())
.then(res => {
this.setState({docs: res.docs, loaded: true})
}, error => {
this.setState({loaded: true, error: JSON.parse(error.message)})
})
}
render() {
const { docs, loaded, error } = this.state
const { selectCog } = this.props
if(!loaded) {
return (
<div>Loading. Please wait...</div>
)
}
if(error) {
console.log(error)
return (
<div>Something broke</div>
)
}
return (
<>
Cogs:
<ul>
{docs.map((cog,index) => {
return (
<li key={index}>
<Cog name={cog.name} documentation={cog.documentation} commands={cog.commands} selectDoc={selectCog} onTriggerOpening={() => selectCog(cog)}></Cog>
</li>
// <li><Collapsible onTriggerOpening={() => selectCog(cog)} onTriggerClosing={() => selectCog(null)} trigger={cog.name}>
// {cog.documentation}
// </Collapsible>
// </li>
)
})}
{/* {docs.map((cog, index) => { */}
{/* return ( */}
{/* <li key={index}><a onClick={() => selectCog(cog)}>{cog.name}</a></li>
)
// })} */}
</ul>
</>
)
}
}
export default CogSelector
the collapsible begins to open on clicking, then it calls the selectCog function which tells it's parent that a cog has been selected, which causes the parent to rerender which causes the following code to run
class DocumentDisplayer extends React.Component{
constructor(props) {
super(props)
this.state = {
cog: null
}
autoBind(this)
}
selectCog(cog) {
this.setState({cog})
}
render(){
const { cog } = this.state
const cogSelector = (
<CogSelector selectCog={this.selectCog}/>
)
if(!cog) {
return cogSelector
}
return (
<>
<div>
{cogSelector}
</div>
<div>
{cog.name} Documentation
</div>
<div
dangerouslySetInnerHTML={{__html: cog.documentation}}>
</div>
</>
)
}
}
export default DocumentDisplayer
hence the cogSelector is rerendered, and it is no longer collapsed. I can then click it again, and it properly opens because selectCog doesn't cause a rerender.
I'm pretty sure this is just some horrible design flaw, but I would like my parent component to rerender without having to rerender the cogSelector. especially because they don't take any state from the parent. Can someone point me to a tutorial or documentation that explains this type of thing?
Assuming that Collapsible is a stateful component that is open by default I guess that the problem is that you use your component as a variable instead of converting it into an actual component ({cogSelector} instead of <CogSelector />).
The problem with this approach is that it inevitably leads to Collapsible 's inner state loss because React has absolutely no way to know that cogSelector from the previous render is the same as cogSelector of the current render (actually React is unaware of cogSelector variable existence, and if this variable is re-declared on each render, React sees its output as a bunch of brand new components on each render).
Solution: convert cogSelector to a proper separated component & use it as <CogSelector />.
I've recently published an article that goes into details of this topic.
UPD:
After you expanded code snippets I noticed that another problem is coming from the fact that you use cogSelector 2 times in your code which yields 2 independent CogSelector components. Each of these 2 is reset when parent state is updated.
I believe, the best thing you can do (and what you implicitly try to do) is to lift the state up and let the parent component have full control over all aspects of the state.
I solved this using contexts. Not sure if this is good practice but it certainly worked
render() {
return (
<DocContext.Provider value={this.state}>{
<>
<div>
<CogSelector />
</div>
{/*here is where we consume the doc which is set by other consumers using updateDoc */}
<DocContext.Consumer>{({ doc }) => (
<>
<div>
Documentation for {doc.name}
</div>
<pre>
{doc.documentation}
</pre>
</>
)}
</DocContext.Consumer>
</>
}
</DocContext.Provider>
)
}
then inside the CogSelector you have something like this
render() {
const { name, commands } = this.props
const cog = this.props
return (
//We want to update the context object by using the updateDoc function of the context any time the documentation changes
<DocContext.Consumer>
{({ updateDoc }) => (
<Collapsible
trigger={name}
onTriggerOpening={() => updateDoc(cog)}
onTriggerClosing={() => updateDoc(defaultDoc)}>
Commands:
<ul>
{commands.map((command, index) => {
return (
<li key={index}>
<Command {...command} />
</li>
)
}
)}
</ul>
</Collapsible>
)}
</DocContext.Consumer>
)
}
in this case it causes doc to be set to what cog was which is a thing that has a name and documentation, which gets displayed. All of this without ever causing the CogSelector to be rerendered.
As per the reconciliation algorithm described here https://reactjs.org/docs/reconciliation.html.
In your parent you have first rendered <CogSelector .../> but later when the state is changed it wants to render <div> <CogSelector .../></div>... which is a completely new tree so react will create a new CogSelector the second time

React: how to propagate state to enclosing parent

I have 2 classes to provide the modal-dialog functionality:
import React from 'react'
import Modal from 'react-modal'
export default class ModalBase extends React.Component {
state = { show:false }
handleOpen = opts => {
this.setState( { ...opts, show:true } )
console.info( 'ModalBase handleOpen', this.constructor.name, 'show', this.state.show )
}
handleClose = () => this.setState( { show:false } )
render() {
console.info( 'ModalBase render show', this.state.show )
return <Modal isOpen={this.state.show} onRequestClose={this.handleClose} className="Modal" overlayClassName="Overlay">
{this.props.children}
</Modal>
}
}
and
export default class InfoPopup extends ModalBase {
state = { ...this.state, tech:{} }
render() {
console.info('InfoPopup render show', this.state.show)
return (
<ModalBase>
<div/><div/>
</ModalBase>
)
}
}
When I call InfoPopup.handleOpen({a:42}), the following shows up in the console:
ModalBase handleOpen InfoPopup show true
InfoPopup render show true
ModalBase render show false
so, the ModalBase's state.show is not changed and hence the popup is not shown.
How shall I properly propagate the state to enclosing parent object?
TIA
Use composition instead of inheritance
From the React docs:
React has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components.
See: https://reactjs.org/docs/composition-vs-inheritance.html
So export default class InfoPopup extends ModalBase is not advised.
1. Let InfoPopup render ModalBase but keep track of open/close state
You could turn it around and have a generic BaseModal component for modal styling that you pass props such as title and content. The InfoPopup keeps track of the opened/closed state. From the same React docs page:
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
2. Render ModalBase and pass the type of modal as prop
You could also always render ModalBase for an info, warning, error modal etc. Then you pass the type of modal as prop to ModalBase. ModalBase determines some specifics based on that type prop.
3. use a render prop
Described here: https://reactjs.org/docs/render-props.html
Let ModalBase accept a function as children prop.
So in InfoPopup:
<ModalBase>
{({ toggle }) => (
<button onClick={toggle} />
)}
</ModalBase>
And in ModalBase:
render() {
return <Modal ...>{this.props.children({ toggle: this.openOrClose })}</Modal>
}
4. Pass a component to ModalBase to render when open
A bit of a variant on 2. You could also pass a component as prop to ModalBase that it should show when it's open.
<ModalBase
modalContent={<InfoPopup />}
/>

Using the React Children code example is not working

"Using the React Children API" code example is not working, tried several syntax options, seems the problem is not quite clear.
http://developingthoughts.co.uk/using-the-react-children-api/
class TabContainer extends React.Component {
constructor(props) {
super();
this.state = {
currentTabName: props.defaultTab
}
}
setActiveChild = (currentTabName) => {
this.setState({ currentTabName });
}
renderTabMenu = (children) => {
return React.Children.map(children, child => (
<TabMenuItem
title={child.props.title}
onClick={() => this.setActiveChild(child.props.name)}
/>
);
}
render() {
const { children } = this.props;
const { currentTabName } = this.state;
const currentTab = React.Children.toArray(children).filter(child => child.props.name === currentTabName);
return (
<div>
{this.renderTabMenu(children)}
<div>
{currentTab}
</div>
</div>
);
}
}
When I changed code like this, it compiles finally
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
const TabMenuItem = ({ title, onClick }) => (
<div onClick={onClick}>
{title}
</div>
);
class TabContainer extends React.Component {
constructor(props) {
super();
this.state = {
currentTabName: props.defaultTab
}
}
setActiveChild = ( currentTabName ) => {
this.setState({ currentTabName });
}
renderTabMenu = ( children ) => {
return React.Children.map(children, child => (
<TabMenuItem
title={child.props.title}
onClick={() => this.setActiveChild(child.props.name)}
/>
))
}
render() {
const { children } = this.props;
const { currentTabName } = this.state;
const currentTab = React.Children.toArray(children).filter(child =>
child.props.name === currentTabName);
return (
<div>
{this.renderTabMenu(children)}
<div>
{currentTab}
</div>
</div>
);
}
}
ReactDOM.render(<TabContainer />, document.getElementById("root"));
Not quite experienced with JS and React, so my questions:
1) should this.setActiveChild be used as this.props.setActiveChild?
2) renderTabMenu = ( children ) or renderTabMenu = ({ children })
3) how to fill this page with some content? I don't see any physical children actually present =)
4) don't get the point why bloggers put the code with errors or which is difficult to implement, very frustrating for newcomers
5) any general guidance what can be not working in this example are welcome
Using React.Children or this.props.children can be a bit of a level up in your understanding of React and how it works. It'll take a few tries in making a component work but you'll get that aha moment at some point. In a nutshell.
this.props.children is an array of <Components /> or html tags at the top level.
For example:
<MyComponent>
<h1>The title</h1> // 1st child
<header> // 2nd child
<p>paragraph</p>
</header>
<p>next parapgraph</p> // 3rd child
</MyComponent>
1) should this.setActiveChild be used as this.props.setActiveChild?
Within the TabContainer any functions specified within it need to be proceeded with this. Within a react class this refers to the class itself, in this case, TabContainer. So using this.setActiveChild(). will call the function within the class. If you don't specify this it will try to look for the function outside of the class.
renderTabMenu = ( children ) or renderTabMenu = ({ children })
renderTabMenu is a function which accepts one param children, so call it as you would call it as a normal function renderTabMenu(childeren)
How to fill this page with some content? I don't see any physical children actually present =)
Here's where the power of the TabsContainer comes in. Under the hood, things like conditional rendering happen but outside of it in another component you specify the content. Use the following structure to render home, blog, and contact us tabs.
<TabsContainer defaultTab="home">
<Tab name="home" title="Home">
Home Content
</Tab>
<Tab name="blog" title="Blog">
Blog Content
</Tab>
<Tab name="contact" title="Contact Us">
Contact content
</Tab>
</TabsContainer>
I know how hard it is to make some examples work especially when you are starting out and are still exploring different concepts that react has to offer. Luckily there's stack overflow :).
Here's real live example to play around with, visit this CodeSandBox.

Resources