0

I am building a react on rails app. I have a button on the page that user can indicate whether they want to join a meet up or not. Clicking "join" button should create a rsvp relation between the current user and an event, and the button will be switched to "Leave", if user then click on the "Leave" button, this relationship will be deleted from the rails backend. After messing around my react component, my "Join" button doesn't trigger the "onSubmit" function, and the "Leave" button seems to return an error saying "submission form cancelled because form is not connected". I'd appreciated a lot if any one can help me clean my logic.

import React from 'react'

class EventsIndexContainer extends React.Component {
 constructor(props) {
 super(props)
 this.state = {
  active: props.rsvp
 }
 this.toggleButton = this.toggleButton.bind(this)
 this.handleRsvpSubmit = this.handleRsvpSubmit.bind(this)
 this.handleRsvpDelete = this.handleRsvpDelete.bind(this)
}

toggleButton() {
 this.setState({active: !this.state.active})
}

handleRsvpSubmit(event) {
 event.preventDefault()
 let formPayLoad = {
  user_id: this.props.current_user.id,
  event_id: this.props.selectedId
 }
 this.props.addRsvp(formPayLoad)
}

handleRsvpDelete() {
 fetch(`/api/v1/rsvps/${this.props.selectedId}`, {
  method: 'DELETE'}
 )
}

render() {
 let button
 let joinButton =
 <form onSubmit={this.handleRsvpSubmit}>
  <button type="button" onClick={() => (this.props.handleSelect(), 
  this.toggleButton())}>Join</button>
 </form>

 let leaveButton =
  <button type="button" onClick={() => (this.toggleButton(), 
  this.handleRsvpDelete)}>Leave</button>

 button = this.state.active? leaveButton : joinButton

 return(
   <div>
      <h4>{this.props.location} - {this.props.meal_type} at 
      {this.props.time}</h4>
      <p>{this.props.group.name}</p>
      {button}
      <button>See who is going</button>
  </div>
 )
}
}

export default EventsIndexContainer

This is the parent container:

import React from 'react'
import GroupIndexContainer from './GroupIndexContainer'
import EventsIndexContainer from './EventsIndexContainer'

class MainContainer extends React.Component {
 constructor(props) {
 super(props)
 this.state = {
  groups: [],
  current_user: null,
  events: [],
  rsvps: [],
  selectedId: null
 }
 this.fetchGroups = this.fetchGroups.bind(this)
 this.fetchEvents = this.fetchEvents.bind(this)
 this.handleSelect = this.handleSelect.bind(this)
 this.addRsvp = this.addRsvp.bind(this)
 this.fetchRsvps = this.fetchRsvps.bind(this)
 }

 componentDidMount() {
    fetch('api/v1/users.json', {
      credentials: 'same-origin',
      method: "GET",
      headers: { 'Content-Type': 'application/json' }
    })
    .then(response => response.json())
    .then(data => {
      this.setState ({current_user: data.user})
    })
    .then(this.fetchGroups())
    .then(this.fetchEvents())
    .then(this.fetchRsvps())
  }

  fetchGroups() {
    fetch('/api/v1/groups', {
      credentials: 'same-origin',
      method: "GET",
      headers: { 'Content-Type': 'application/json' }
    })
    .then(response => response.json())
    .then(data => {
      this.setState({groups: data.groups})
    })
  }

  fetchEvents() {
    fetch('/api/v1/events', {
      credentials: 'same-origin',
      method: "GET",
      headers: { 'Content-Type': 'application/json' }
    })
    .then(response => response.json())
    .then(data => {
      this.setState({events: data})
    })
  }

  fetchRsvps() {
    fetch('/api/v1/rsvps', {
      credentials: 'same-origin',
      method: "GET",
      headers: { 'Content-Type': 'application/json' }
    })
    .then(response => response.json())
    .then(data => {
      this.setState({rsvps: data.rsvps})
    })
  }

  handleSelect(id) {
    this.setState({selectedId: id})
  }

  addRsvp(formPayLoad) {
    fetch('/api/v1/rsvps', {
      method: 'POST',
      credentials: 'same-origin',
      headers: { 'Accept': 'application/json', 'Content-Type': 'application/json'},
      body: JSON.stringify(formPayLoad)
    })
  }

  render() {
    let groups = this.state.groups.map((group) => {
      return (
        <GroupIndexContainer
          key={group.id}
          id={group.id}
          name={group.name}
        />
      )
    })

    let rsvp_ids = this.state.rsvps.map(rsvp => rsvp.event_id)
    let events = this.state.events.map((event) => {
      return(
        <EventsIndexContainer
          key={event.id}
          id={event.id}
          rsvp={rsvp_ids.some(rsvp_id => rsvp_id == event.id) ? true : false}
          location={event.location}
          meal_type={event.meal_type}
          time={event.time}
          group={event.group}
          current_user={this.state.current_user}
          user={event.user}
          selectedId={this.state.selectedId}
          addRsvp={this.addRsvp}
          handleSelect={() => this.handleSelect(event.id)}
        />
      )
    })

    return(
      <div className="wrapper">

        <div className="groups-index">
          <h3>Your Groups:</h3>
          {groups}
        </div>

        <div className="events-index">
          <h3>What's happening today...</h3>
          {events}
        </div>

      </div>
    )
  }
}

       export default MainContainer
Olivia
  • 195
  • 4
  • 14

1 Answers1

0

The button built before returning in the render function is either a form or a button. I would suggest to simply check the state of your component, avoid using the form (which is not inserted in the DOM, hence the "submission form cancelled because form is not connected" message).

Basically, your code will be much simpler if you use the onClick function of a button. You won't have to deal with the button types submit or button that will trigger onSubmit for the former or not for the latter as per : Difference between <input type='button' /> and <input type='submit' />

Also, using arrow functions in components properties is not a good practise, as well documented here : Why shouldn't JSX props use arrow functions or bind?

So I would suggest in a second time to change your onClick property to something like onClick={ this.handleLeave }, bind handleLeave in the constructor like you did for other functions, and handle the work there (and do the same for handleJoin).

I tried to rework a bit your code in the following snippet, hope this will help!

class EventsIndexContainer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      active: props.rsvp
    }
    this.toggleButton = this.toggleButton.bind(this)
    this.handleRsvpSubmit = this.handleRsvpSubmit.bind(this)
    this.handleRsvpDelete = this.handleRsvpDelete.bind(this)
    
    // Stub
    this.handleSelect = this.handleSelect.bind(this)
  }
  
  handleSelect() {
    console.log("handleSelect called");
  }
  
  toggleButton() {
    this.setState({active: !this.state.active})
  }
  
  // event argument removed here, wasn't used anyway
  handleRsvpSubmit() {
    console.log("handleRsvpSubmit called")
  }
  
  handleRsvpDelete() {
    console.log("handleRsvpDelete called")
  }
  
  render() {
    return(
      <div>
        <h4>Hello</h4>
        <p>Group name</p>
        { this.state.active ?
          <button type="button" onClick={() => (this.toggleButton(), this.handleRsvpDelete())}>Leave</button>
        :
          <button type="button" onClick={() =>(this.handleSelect(),this.toggleButton(), this.handleRsvpSubmit())}>Join</button>
        }
        <button>See who is going</button>
      </div>
      )
    }
  }
  
ReactDOM.render(
  <EventsIndexContainer rsvp={ false } />,
  document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container">
    <!-- This element's contents will be replaced with your component. -->
</div>
Philippe Sultan
  • 2,111
  • 17
  • 23