2

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

Chris
  • 135
  • 2
  • 14
  • @NickG That's part of the issue - I'm not sure where to render it. I guess it should be rendered in App.js somewhere so it's accessible to all child components? I am completely new to React so I'm struggling to visualise it (and put it into words!) – Chris Jun 26 '18 at 14:49

1 Answers1

4

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.

Gegenwind
  • 1,388
  • 1
  • 17
  • 27
  • Thanks so much for the detailed response. Unfortunately I'm still struggling, and I'm sure it's just to do with passing data via props and states. I'm continuing to work with your suggestion though, and I'll come back here when I've made some progress! – Chris Jun 27 '18 at 09:14
  • If you set up a sandbox with your code I will be happy to help you (https://codesandbox.io/) – Gegenwind Jun 27 '18 at 12:10
  • Thanks so much for the offer of help, I'm very grateful. I actually started a brand new project from scratch to try and understand how properties and states work in React. My sandbox: https://codesandbox.io/s/7mo4124l51 I used [this video](https://youtu.be/AnRDdEz1FJc) to get started incrementing a counter in a parent component by clicking on a button in a child component. I added another level to that ('Grandparent' in the sandbox) and it seems to work. What I am stuck on is how to make the "Open Modal with X" buttons open the modal and print in it "A" or "B" respectively. Thanks so much! – Chris Jun 27 '18 at 14:48
  • ... I think that once I can get rid of the "Show Modal" button, and use the two "A" and "B" buttons in the Child component to open the modal and pass some data to it, I'll be on the right track. At the moment I keep going round and round in circles. – Chris Jun 27 '18 at 14:49
  • @Chris: I think you might want to read up on state and props. I updated your example with the correct way to pass state to children: https://codesandbox.io/s/llpvn5nxz9 Basically: Only your highest order component uses state. And only this component implements a function that changes state. You then pass the state, e.g. counter, to children. There, counter is available as this.props.counter. A prop can not be changed but a child can call a parent function to change the state - as you did with the updateCounter-function. – Gegenwind Jun 27 '18 at 17:00
  • That summary has suddenly made something "click". I've cleared out the superfluous methods, and I've applied the principle you mentioned and I've got the modal to open from the top-level App component as well as the bottom-level Child component - brilliant! Thank you! https://codesandbox.io/s/m35qj0mr5j I think I'm just stuck on one more thing for now: how do I get the value of `someVar` (a state of the Parent component) into the ModalTest component, when the ModalTest component is activated by the button in the Child component? – Chris Jun 27 '18 at 21:59
  • Happy to see you are getting ahead. Again, there are different ways to do it. As of now, the modal gets its props in the app component. So this is where you need to pass data to the modal. You could add a prop like data={something} in your App component and add a function setModalData() which you pass done just as you did with open/close functions. As you can see this gets a bit messy when you do a lot of passing so maybe there is some restructuring in order later on. – Gegenwind Jun 28 '18 at 06:34
  • Aha I see! I think I've got it now: https://codesandbox.io/s/m35qj0mr5j I used the top answer [here](https://stackoverflow.com/questions/29810914/react-js-onclick-cant-pass-value-to-method) to extract the click handler function of the Child button to allow me to add parameters to the function. Thank you **so much** for your help (and for not just providing the answers straight away) - it's very much appreciated. – Chris Jun 28 '18 at 07:32