Editing a record - Part 2

Linked Projects:

Calreact
 
Lesson code - https://github.com/learnetto/calreact/tree/m7-react-router

Now that we’ve refactored our AppointmentForm state management in the previous lesson, we can go ahead and make editing work.

If I go to the edit route, the form loads fine.

[screenshot 0:13-14]

What we want to happen here, is that the form should be pre-populated with the title and appt_time values of that appointment. And we want to change the title to Edit appointment instead of Make a new appointment, and finally also change the submit button text to Update Appointment.

Let’s start by loading the appointment data in the form. Once again, we’ll use componentDidMount and fetch the data for the appointment there.

Let’s copy componentDidMount from Appointment.jsx and paste it into our AppointmentForm component.

componentDidMount () {
      if(this.props.match) {
        $.ajax({
   type: "GET",
   url: `/appointments/${this.props.match.params.id}`,
   dataType: "JSON"
        }).done((data) => {
          this.setState({appointment: data});
        });
      }
    }

But we’ll need to change the setState call to update the correct values in this component’s state.

Change:

this.setState({appointment: data});

to:

this.setState ({
                title: {value: data.title, valid: true }
});

Because we’re going to assume that the data coming in from the database is already valid. 

And also add:

this.setState ({
                title: {value: data.title, valid: true }
                appt_time: { value: data.appt_time, valid: true }
});

Then go back and load the edit route again.

The form is now being populated with the appointment data.

But also because we’re assuming the loaded values are valid, we run into one issue. 

For example, the appointment time for this particular appointment is in the past, so it’s actually invalid as per our validation rules.

It was valid when it was created but not anymore.

But if I edit the title, the form validation kicks in and the button gets enabled.

[screenshot 1:44]

That’s because when we change a field value, it only triggers the validation for that field, not for all the fields.

Which is how it should be - we don’t want to validate all the fields every time one of them changes.

So now the form is actually allowing me to submit an invalid appointment time.

We could change that by checking for the validity of the appointment data before calling setState.

But that would add a lot of extra code for not much benefit.

I think it’s better if we leave it as it is and let the server do the validation in this one edge case.

So if I submit the form, appt_time will fail validation on the server, and the user will know it needs to be fixed.

[screenshot: 2:21]

The only thing we should fix is the submit button. It should be disabled.

The reason it’s still enabled is that when we get an error from the server, we don’t change the form validity state.

So let’s fix that in the handleFormSubmit method.

In fail in AppointmentForm.jsx, let’s set formValid to false:

 handleFormSubmit = (e) => {
      e.preventDefault();

. . .

this.setState({formErrors: response.responseJSON, formValid: false})

Now if I submit the form, I get the error message and the button also gets disabled.

[screenshot: 2:59-3:00]

Before going further, there’s one little issue I’d like to fix.

You see right now we can navigate to the edit route from the homepage, but we can’t load an edit route directly. If I refresh this page, I get this unknown action error from the controller because we don’t have an edit action.

[screenshot: 3:12]

So let’s add that:

def show
   @appointment = Appointment.find(params[:id])
   render json: @appointment 
end

def edit
    render :index
end

So now even on the edit page, we simply render the index view, which if you remember simply renders AppRouter onto the page.

Now if I refresh, the edit route will also load fine.

The page loads. Now we need to tweak the form to make it work for updating an appointment.

Right now, submitting the form will create a new appointment.

So we need to change the handleFormSubmit method so that it can also update an existing appointment.

We can tell whether the form is being used for creating or editing an appointment based on the route.

We can check for the value of this.props.match in AppointmentForm.jsx.

If we look in the console, we can see match.path:

[screenshot: 4:17]

So we can check:

handleFormSubmit = (e) => {
      e.preventDefault();
this.props.match.path === '/appointments/:id/edit' ?
  this.updateAppointment() :
  this.addAppointment();
}

We'll define updateAppointment in a moment, and addAppointment will simply make the previous AJAX call, so let’s move that into a new method.

addAppointment () {
  const appointment = {title: this.state.title.value
  ...
}

And now, let’s define updateAppointment.

We’ll start by making a copy of addAppointment, changing the name to updateAppointment, and then changing the AJAX call.

Let’s remove the call to handleNewAppointment and change $.post to $.ajax, using a type of PATCH (we can also use PUT instead, as they're interchangeable in Rails).

We'll set the URL to:

url: `/appointments/${this.props.match.params.id}`

Data will be:

data: {appointment: appointment}

And on success, let’s just log a message to the console:

console.log('appointment updated.')

We could add a status message to show to the user, but this will do for now.

The final method should look as follows:

updateAppointment () {
  const appointment = {title: this.state.title.value,
                       appt_time: this.state.appt_time.value};
  $.ajax({
    type: "PATCH",
    url: `/appointments/${this.props.match.params.id}`,
    data: {appointment: appointment}
  })
  .done((data) => {
    console.log('appointment updated!');
    this.resetFormErrors();
  })
  .fail((response) => {
    this.setState({formErrors: response.responseJSON,
                    formValid: false});
  });
}

Then let’s add an update action in the controller to handle our patch request.

def update
  @appointment = Appointment.find(params[:id])
  if @appointment.update(appointment_params)
    render son: @appointment
  else
    render son: @appointment.errors, status: :unprocessable_entity
  end
end

Let’s try this. Let’s choose a new date and submit. Ok, appointment updated!

[screenshot: 6:30]

Now that we’ve got the form working, let’s fix the button text and the page title.

We’ll do it by adding a new state property called editing, which will indicate whether we’re editing an appointment or creating a new one.

So let's add another state value editing in componentDidMount:

editing: this.props.match.path === '/appointments/:id/edit'

and set it to false in the initial state:

editing: false

So now in the handleFormSubmit method, we can change 

this.props.match.path === '/appointments/:id/edit'

to

this.state.editing

And to change the page title, in the render method, replace

<h2>Make a new appointment</h2>

with:

<h2>
  {this.state.editing ?
    'Update appointment' :
    'Make a new appointment' }
</h2>

Ok, finally, let’s do the button. Change

value='Make Appointment'

to:

value={this.state.editing ?
        'Update Appointment' :
        'Make Appointment'}

That works but I think there is one little bug on our homepage.

When I click back to the homepage from the edit page, the appointments list is not loading.

Let’s look in the console.

Cannot read property 'map' of null at AppointmentsList

[screenshot 8:57]

It’s a required prop, but there’s no default value here. So let’s add the default props. I’ll just copy paste it from the appointments component.

So in AppointmentsList:

AppointmentsList.defaultProps = {
  appointments: []
}

That should fix this error.

If I go to the homepage, then edit and back and it all works fine.

Editing appointments works.

Liked this tutorial? Get more like this in your inbox