For part 4, I will be adding location editing functionality to full stack app described in parts 1–3 linked below. Be sure to view these links before proceeding with instructions for part 4, below:)

part 1: https://benton-westergaard.medium.com/build-a-simple-fullstack-app-using-rails-and-react-460ebec3a1bc

part 2: https://benton-westergaard.medium.com/simple-full-stack-app-with-rails-react-ii-bd850f53ad3e

part 3: https://benton-westergaard.medium.com/simple-full-stack-app-with-rails-react-iii-33a136a70302

Rather than create a new component to edit existing locations, I’m going to use conditional rendering to modify the LocationForm so that it will modify existing locations.

I’m going to start with the backend, by adding an update method to my locations_controller.rb. While here, I’m going to create a private method which includes a find_location method, which will be helpful for both the new update method, as well as the existing destroy method. First step is to add a before_function at the top:

before_action :find_location, only: [:update, :destroy]

This means the before action will only run when the update and destroy methods are called.

Now, I create a private method for find_location, below the line ‘private’ (which is understood by rails):

privatedef find_location
@location = Location.find(params[:id])
end

I can now shorten my delete method by removing the find line, because the before_action has already called the location. To be clear, it now looks like this:

def destroy
@location.destroy
render status: :no_content
end

I’m also going to create another private method for location_params, to clean up my controller:

def location_params
params.require(:location).permit(:name, :baseball, :basketball, :football, :hockey, :capital, :total_teams)
end

This will allow me to update the create method:

def create
@location = Location.create(location_params)
render json: @location, status: :created
end

And (finally) finish the update using the clean location_params method:

def update
@location = Location.update(location_params)
render json: @location
end

Now, to the front end. I want to add a new button to each location item which, when clicked, will pop up a form which will allow users to edit information associated with that location. For example, I could fix a typo for a team name. To get started, I will add a button to the location item, and basically copy the code for the existing delete button:

<button onClick={handleClick} className="delete-button">DELETE LOCATION</button>
<button className="edit-button">EDIT LOCATION</button>

(I removed the onClick method FOR NOW, just so I don’t accidentally delete all my locations)

I’ll also add a bit of basic styling in App.css so the button stands out from the existing delete button.

.edit-button {
background-color: lightskyblue;
color: white;
}

Now, to make the button open the edit field. To be clear, I want the form to already populate with the existing info for the location. Users expect this, and it’s annoying when an edit function doesn’t make it obvious how any changes will alter existing material. I’ll use a hook and set state, so I’ll need to first bring in setState while importing react:

import React, {useState} from 'react';

and create some simple logic that basically flips the switch to ‘off’ or ‘on’, depending on it’s current position:

const [isToggled, setIsToggled] = useState(false)
const handleToggle = (event) => setIsToggled(!isToggled)

And also add a function to the delete button:

<button onClick={() => setIsToggled(!isToggled)} className="edit-button">EDIT LOCATION</button>

It’s a good idea to check this by going into browser components, selecting a location, and clicking the edit button. If the new code is working correctly, the hooks State should flip from ‘false’ to ‘true’ (or vice versa) with each click of the button.

Now, I need to create a turnery which shows the card in edit mode if the ‘edit’ button is clicked. Because the return is already pretty long, I’m going to move it up into a function called locationCard:

const locationCard = () => (
<li className="location-item">
<h2>Location: {name}</h2>
<h4>MLB team(s): {baseball}</h4>
<h4>NBA team(s): {basketball}</h4>
<h4>NFL team(s): {football}</h4>
<h4>NHL team(s): {hockey}</h4>
<h4>Total professional teams: {total_teams}</h4>
<h4>Any teams use state capital in their title? {capital}</h4>
<button onClick={handleClick} className="delete-button">DELETE LOCATION</button>
<button onClick={handleToggle} className="edit-button">EDIT LOCATION</button>
</li>

…and put the turnery in the return:

return isToggled ? <LocationForm /> : locationCard()

Now, I can edit the form when clicking the ‘edit’ button. However, it still looks like ‘add new location’, so I need to add something which makes it obvious to users that they’re editing and existing location.

First, I’m going to send down a new prop for location. I think the easiest way to do this is to create a new const for the destructured location:

const location = {id, name, baseball, basketball, football, hockey, total_teams, capital}

Then, in the return, I send down the prop (new part bolded):

return isToggled ? <LocationForm location={location} /> : locationCard()

Now, in locationForm.js, I add another turnery to the render for the h2, so that if there is a location called, it shows “Edit Location”, and if not, “Add a New Location” (new parts bolded):

{this.props.location ? <h2>Edit Location</h2> : <h2>Add a new Location</h2>}

Now, to pull in existing data for a given location, I add a componentDidMount to the location Form, and setState if the data exists:

componentDidMount(){
const{location} = this.props
if(this.props.location){
const{id, name, baseball, basketball, football, hockey, capital, total_teams} = location
this.setState({
id,
name,
baseball,
basketball,
football,
hockey,
capital,
total_teams
})
}
}

Now, in App.js, I add a function to update a location, which checks to see if the location.id is equal to that of an existing location.id. If so, the location is updated. If not, it isn’t:

updateLocation = (updatedLocation) => {
let locations = this.state.locations.map(location => location.id === updatedLocation.id ? updatedLocation : location)
this.setState({ locations })
}

Of course, adding this function to the render (addition bolded):

<LocationContainer updateLocation={this.updateLocation} deleteLocation={this.deleteLocation} locations={this.state.locations} />

Now, passing this down one level to LocationContainer.js, first add the destructured updateLocation:

export default function LocationContainer({locations, deleteLocation, updateLocation}) {

As well as the the return:

return locations.map(location => <LocationItem key={location.id} {...location} updateLocation={updateLocation} deleteLocation={deleteLocation}/>)

Now, passing down one more level to the location item up top:

export default function LocationItem({id, name, baseball, basketball, football, hockey, total_teams, capital, deleteLocation, updateLocation}){

…and now getting it to the form:

return isToggled ? <LocationForm updateLocation={updateLocation} location={location} /> : locationCard()

Now, I need to make an adjustment since there are two different functions calling the same form. So, in App.js, I modify the return, changing addLocation to submitAction:

<LocationForm submitAction={this.addLocation} />

…and in LocationForm.js, changing the handleSubmitFunction by replacing addLocation with submitAction, it will call the addSubmit function:

this.props.submitAction(this.state)

Now, in LocationItem.js, I do the same thing, but the program knows which function is being called based on it’s file location in the component hierarchy.

return isToggled ? <LocationForm submitAction={updateLocation} location={location} /> : locationCard()

DIFFERENT FUNCTION, SAME PROP.

Finally, I need LocationItem.js to understand which function is being called by using another turnery:

return isToggled
? <LocationForm
handleToggle={handleToggle}
submitAction={updateLocation}
location={location}
/> : locationCard()

Now, edits are saved. They don’t persist, but I’ll save that for the next blog. This is getting absurdly long:)

front end: https://github.com/bwesterg/sportsball-frontend

back end: https://github.com/bwesterg/sportsball_backend