Forms: Editing Records Part 2

Exercises

Objective

Remember, the way forms work is a two-part story. The first part is all about displaying the form. The second part is about what happens when you enter information in the form and submit it. The form data needs to get sent back into the app so it can be saved in the database. To handle that, our app needs another route and a corresponding action. By convention, that action is called update because we're updating an existing record in the database. Once the movie has been updated, we'll redirect the browser to the movie's show page.

If you like diagrams (can you tell we do?), here's our goal:

Let's jump right into it!

1. Add a Route

Let's try submitting the edit form and see where it leads us...

  1. Submit the form by clicking the "Update Movie" button and you should see the following error:

    Routing Error
    
    No route matches [PATCH] "/movies/1"

    Well, the error looks familiar, but there's something new this time. Notice that this is a PATCH request rather than a GET request as we've been seeing up to this point. In HTTP, GET requests are used to read things from the server. PATCH requests, by comparison, are used to update (write) things in the server. And Rails knows that we're trying to update something.

  2. The error clearly indicates that submitting the form will send the form data in a PATCH request to /movies/1. Let's recheck which routes we already have:

    Helper         HTTP Verb        Path                  Controller#Action
    root_path         GET        /
    movies_path       GET        /movies(.:format)           movies#index
    movie_path        GET        /movies/:id(.:format)       movies#show
    edit_movie_path   GET        /movies/:id/edit(.:format)  movies#edit
    

    We have a route that recognizes /movies/1, but it expects the HTTP verb to be GET, not PATCH. It's important to remember that the router matches requests based on the unique combination of both the HTTP verb and the URL. The same URL can do something different based on the HTTP verb. That being the case, we're missing a route.

  3. Add a route that sends PATCH requests for movies/1, for example, to the update action of the MoviesController. Don't worry about giving the route a name because we don't need to generate a link for it. (The form_with helper will handle that for us.)

    Show Answer

    Hide Answer

    patch "movies/:id" => "movies#update"
    

    Listing the defined routes should then give you five total routes (we'll ignore the other default routes):

    Helper         HTTP Verb        Path                  Controller#Action
    root_path         GET        /
    movies_path       GET        /movies(.:format)           movies#index
    movie_path        GET        /movies/:id(.:format)       movies#show
    edit_movie_path   GET        /movies/:id/edit(.:format)  movies#edit
                      PATCH      /movies/:id(.:format)       movies#update
    
  4. Next, back over in your the browser, click the "Update Movie" button again to submit the form and you should see an old friend:

    Unknown action
    
    The action 'update' could not be found for MoviesController

2. Implement the Update Action

Taking that subtle hint, we need to define the update action in the MoviesController. The update action needs to pick up the submitted form data from the request in order to actually update the movie in the database.

  1. Just to see the form data that's being submitted, define the update action and try this fail trick we used in the video:

    def update
      fail
    end
    

    The fail method in Ruby intentionally raises an exception. It's often used as a poor (lazy) person's debugger. When fail is called in Rails app, execution is halted, Rails intercepts the exception, and spills its guts on an error page in the browser. In addition to all the request parameters, the error page also displays any submitted form data.

  2. Now submit the form with data again and you'll get an error (or a debugging page, depending on how you look at it). Check out the stuff under the "Request" heading and you should see something like this:

    {"_method"=>"patch",
     "authenticity_token"=>"Zd7oa0CPg1P1/ObENZb22VjQHrbJS1loXrxp42lSNdw=",
     "movie"=>
       {"title"=>"Iron Man",
        "description"=>"When wealthy industrialist Tony Stark...",
        "rating"=>"PG-13",
        "released_on(1i)"=>"2008",
        "released_on(2i)"=>"5",
        "released_on(3i)"=>"2",
        "total_gross"=>"585366247.0"},
     "commit"=>"Update Movie",
     "id"=>"1"}
    

    What we're seeing here is a representation of a Ruby hash: a set of keys and values. The highlighted lines are what we're most interested in. Notice that the hash includes an id key whose value is the id of the movie we want to update. The hash also includes a movie key. Here's where things get interesting. The value of the movie key is actually another (nested) hash. And that hash contains the form data: the movie attribute keys and values we want to change. In other words, the form data is scoped to movie in the request parameters hash. So in the request parameters hash we have all the information we need to actually update the movie in the database!

    Now, how exactly do we get access to this information in our update action? When Rails receives a request—be it a GET, PATCH, or whatever—it automatically packages the request parameters in a hash called params that's accessible in an action.

    Inside the update action, to get the movie id we use:

    params[:id]
    

    That returns 1 in this case. Note that in the original hash the key is the string "id", but since we're using the params hash we can use the symbol :id. The params hash lets us look up values using either string or symbol keys, but it's more idiomatic to use symbols.

    In the same way, to get the names and values of the movie attributes submitted from the form, we use:

    params[:movie]
    

    That returns a hash of movie attribute keys and values:

    {
      "title"=>"Iron Man",
      "description"=>"When wealthy industrialist Tony Stark...",
      "rating"=>"PG-13",
      "released_on(1i)"=>"2008",
      "released_on(2i)"=>"5",
      "released_on(3i)"=>"2",
      "total_gross"=>"585366247.0"
    }
    

    Handy, dandy!

  3. Now that you know how to pull out the form data from the request parameters, remove the fail line from the update action and instead do two things. First, find the movie being updated and assign it to an instance variable called @movie. Then use the submitted form data to update the movie's attributes and save it to the database.

    First you'll need to fetch the movie to be updated from the database using the value in params[:id]. Then, to update its attributes with the form data, recall that all models that inherit from ApplicationRecord have an update method. That method takes a hash of attribute names and values. Conveniently, accessing params[:movie] gives you the form data represented as a hash of movie attribute names and values. Combine those two things, and you can use the form data to update the movie and save it back to the database in one line of Ruby!

    def update
      @movie = Movie.find(params[:id])
    
      @movie.update(params[:movie])
    end
    
  4. Now, back in your browser, use the form to edit some details about your first movie. For example, you may want to rename "Iron Man" to "Iron Woman" and set its total gross to $1B!

  5. Then submit the form again and this time you should get a different error:

    ActiveModel::ForbiddenAttributesError

    Forbidden! What happened was we took all the movie attributes in the form data and tried to assign them in bulk (using update) to the movie being updated. Rails grabs us by the collar and says: "You sure about that? Do you really want to trust parameters from the big, bad Internet?" It's actually quite trivial for a malicious user (a hacker) to fake form data and end up changing movie attributes that we don't want them to change.

    To prevent that from happening, Rails requires us to explicitly list the attributes that can be mass assigned from form data. We do that by calling the permit method and passing in the list of attributes that are allowed to be mass-assigned from form data, like so:

    params[:movie].permit(:title, :description, :rating, :released_on, :total_gross)
    

    That line of code returns a new hash that includes only the permitted attributes. It also marks the hash as being "permitted" which tells Rails that we've taken the necessary security precautions. In other words, we now have a white list of trusted attributes that can safely be sent through the update method.

    Alternatively, if we want to lock this down a little tighter, we can call the require method on the params hash and pass it the parameter key we expect, like so:

    params.require(:movie).permit(:title, :description, :rating, :released_on, :total_gross)
    

    The require method returns the same hash as we get by accessing params[:movie], but require will raise an exception if the given key (:movie in this case) isn't found in the params object. So using require is preferred because it gives us one more added check on the form data.

    Finally, if you want all of a movie's attributes to be updatable without listing them out, you can call permit! like so

    params.require(:movie).permit!
    

    While convenient, using permit! is risky because all the attributes will always be updatable from form data. Instead, it's better to explicitly list the attributes that can be updated from a form.

  6. In your update action, use the line of code above and assign the result to a variable called movie_params, for example. Then pass that variable as a parameter to the update method.

    Show Answer

    Hide Answer

    def update
      @movie = Movie.find(params[:id])
    
      movie_params =
        params.require(:movie).
          permit(:title, :description, :rating, :released_on, :total_gross)
    
      @movie.update(movie_params)
    end
    
  7. Then submit the form with data again and Rails shouldn't hassle you about the perils of form data on the big, bad web. However, this time the button should become disabled and it will appear as if nothing happened. But something did indeed happen!

    If you have a look in the Terminal or command prompt window where the Rails server is running, you should see something like the following:

    Started PATCH "/movies/1" for ::1 at 2019-05-28 13:42:05 -0600
    Processing by MoviesController#update as HTML
    Parameters: {...}
    Movie Load (0.1ms)  SELECT "movies".* FROM "movies" WHERE "movies"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
    (0.1ms)  begin transaction
    Movie Update (0.3ms)  UPDATE "movies" SET "title" = ?, "total_gross" = ? WHERE "movies"."id" = ?  [["title", "Iron Woman"], ["total_gross", 1000000000.0], ["id", 1]]
    (0.5ms)  commit transaction
    No template found for MoviesController#update, rendering head :no_content

    That output tells you exactly what happened, right down to the SQL UPDATE statement that was used to update the event in the database. (You'll only see the update if you actually change movie attributes in the form.)

    Great! This means the update action successfully update the movie details in the database based on the form data. After doing that, however, Rails went looking for a matching update view template, which we don't have.

    We'll need to figure out what to show in response to the movie being updated, but let's ignore that error for a minute. First let's verify that the movie was indeed updated in the database. Hop into a console session, find the movie you updated, and then check that each attribute has the value you set in the form.

    Show Answer

    Hide Answer

    >> movie = Movie.find(1)
    
    >> movie.title
    => "Iron Woman"
    
    >> movie.total_gross.to_s
    => "1000000000.0"
    

    A billion dollars?! Now that's a hit superhero movie!

3. Redirect to the Show Page

OK, so what should we do after the movie has been successfully updated? Well, it stands to reason that the user would want to see the updated movie's details. And lo and behold, we already have a show action that does that. So let's just redirect the browser back to that URL. That will force the browser to send a new request into our app. The new request will be a GET for /movies/1, for example.

In the update action, after the movie has been updated, redirect to the movie's show page.

Use the redirect_to helper and pass the URL to the show page as a parameter. Remember, you already have a route helper method that will generate the URL to the show page. By using a redirect, the action won't try to render an update view template, which we don't have. Take that, browser!

def update
  @movie = Movie.find(params[:id])

  movie_params =
    params.require(:movie).
      permit(:title, :description, :rating, :released_on, :total_gross)

  @movie.update(movie_params)

  redirect_to @movie
end

Then submit the form one last time! This time the movie should get updated in the database and you should get redirected to the show page where those changes are reflected.

And with that, our two-part, edit-update story comes to an end. Nicely done!

Solution

The full solution for this exercise is in the forms-edit-2 directory of the code bundle.

Bonus Round

Where Did PATCH Come From?

Folks often ask how Rails knows to use the PATCH verb when updating data. The answer lies in the HTML that was generated by the form_with helper. View the page source of the form and hone in on this part of the HTML form:

<form action="/movies/1" method="post">
  <input name="_method" type="hidden" value="patch" />
  ...
</form>

The relevant parts here are the action and the method. The action says that submitting this form will send the data to /movies/1. But remember that the router recognizes requests based on both a URL pattern to match and an HTTP verb. So the method attribute in the form specifies the HTTP verb to use. By default, forms are submitted with the POST HTTP verb as indicated by the value of the method attribute here.

Ah, but the plot thickens: By convention in Rails, POST requests are used to create things. But in this case, Rails knows that we're trying to update a movie, not create a movie. And by convention in Rails, PATCH requests are used to update things. So Rails effectively overrides the HTTP verb by adding a hidden field named _method with a value of patch. (It has to fake it with a hidden field because web browsers can't natively send PATCH requests.)

Anyway, that's where the PATCH comes from. Do you need to understand all that before moving on? Nope, not at all. Sometimes it's just comforting to know what's going on behind the curtain.

What Does a Redirect Do?

It's important to understand that when we submit a form, our application actually ends up handling two requests: a PATCH request and a GET request. Let's take a minute to follow those requests in the server log when you submit the form.

The first request you see is the PATCH that's handled by the update action:

Started PATCH "/movies/1" for ::1 at 2019-05-28 13:51:46 -0600
Processing by MoviesController#update as HTML
Parameters: {...}
Movie Load (0.1ms)  SELECT "movies".* FROM "movies" WHERE "movies"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
(0.0ms)  begin transaction
Movie Update (0.3ms)  UPDATE "movies" SET "title" = ?, "updated_at" = ? WHERE "movies"."id" = ?  [["title", "Iron Man"], ["updated_at", "2019-05-28 19:51:46.740529"], ["id", 1]]
(0.7ms)  commit transaction
Redirected to http://localhost:3000/movies/1
Completed 302 Found in 6ms (ActiveRecord: 1.1ms | Allocations: 3734)

Check out what happens at the end of the request, on the second to the last line. It tells the browser to redirect to http://localhost:3000/movies/1. It does that by sending the browser a URL and an HTTP redirect status code (302) as seen on the last line. It's like telling the browser: "Nothing to see here, move along. Go this way instead."

So the browser issues a brand new GET request for the updated movie, and we see the second request come into our app:

Started GET "/movies/1" for ::1 at 2019-05-28 13:51:46 -0600
Processing by MoviesController#show as HTML
Parameters: {"id"=>"1"}
Movie Load (0.1ms)  SELECT "movies".* FROM "movies" WHERE "movies"."id" = ? LIMIT ?  [["id", 7], ["LIMIT", 1]]
Rendering movies/show.html.erb within layouts/application
Rendered movies/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 201)
Completed 200 OK in 24ms (Views: 22.6ms | ActiveRecord: 0.1ms | Allocations: 15221)

The result of that request is to render the show view template. After doing that, the app sends the generated HTML back to the browser with a 200 HTTP status code which indicates the request was successful.

By following the PATCH-Redirect-GET pattern, we prevent duplicate form submissions, and thereby create a more intuitive interface for our users.

Spend some time watching the server log, and all mysteries will be revealed!

Wrap Up

Congratulations! This was a big step that put together a whole bunch of concepts we learned earlier, and a few new ones. So make sure to reward yourself with a well-deserved break and a tasty snack!

Here's the thing about forms in Rails: They're actually quite elegant once you get comfortable with the conventions. The conventions really do save you time once you grok what's going on, but initially they can feel overwhelming. It's best to start with a simple form and use the errors (and server log) to trace through what's happening at each step. By about your second or third form, everything will click. It just takes some practice to get the hang of it. And that's what this exercise was all about: practice!

Speaking of practicing with forms, in the next section we'll bring all this together again so we can create new movies in the web interface. That'll give us a good opportunity to cement what we've learned about forms and their conventions.

Dive Deeper

For more options on permitting mass-assigned parameters, check out strong parameters.