Forms: Editing Records Part 1

Exercises

Objective

At this point we have some fundamental features under our belt and our movies app is shaping up nicely. The movies are neatly tucked away in a database and we can list and show them in the browser. That's a good start!

Now, what if we want to change a movie's details? The only way to do that currently is to fire up a console session and programmatically change movie attributes. But this is supposed to be a web app! So the next step is to create a web interface for editing (changing) movie details. We'll tackle this in two parts, similar to the way we did it in the video.

First we need to display a web form so a user can edit a movie's details. Doing that involves the following high-level tasks:

  1. Add a route to handle requests for /movies/1/edit, for example.

  2. Generate an "Edit" link on the show page.

  3. Define an edit action in the MoviesController that finds the movie we want to update and displays an HTML form.

  4. Create an edit.html.erb view template that generates the HTML form pre-populated with the movie's details.

Visually, here's what we want to do:

Then, in the second part, we'll need to define an update action in the MoviesController that saves movie changes to the database when the form is submitted.

We have our work cut out for us, so let's get started!

1. Add an Edit Route and Link

Let's start with the URL we want and work our way from the outside-in. By convention in Rails, the URL for editing a movie would be http://localhost:3000/movies/1/edit, for example. To make it easy to navigate there, we'll generate an "Edit" link on the show page.

  1. As a jumping-off point, browse to http://localhost:3000/movies/1/edit and you should get the following error:

    Routing Error
    
    No route matches [GET] "/movies/1/edit"
  2. Fix it by adding a route that sends requests for movies/1/edit, for example, to the edit action of the MoviesController. Make the route flexible enough to recognize any arbitrary movie id (not just 1). We'll need to be able to generate an "Edit" link, so go ahead and name the route edit_movie.

    Remember, to name a route you use the as: option.

    get "movies/:id/edit" => "movies#edit", as: "edit_movie"
    
  3. Now list the defined routes and you should have the following four routes, with the new route listed last:

    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
    
  4. Great! Now use the route helper method to generate an "Edit" link at the bottom of the show template. Once you get the link working, go ahead and copy in the version in the answer that uses HTML elements and class names that trigger the styles in our custom.scss stylesheet.

    Show Answer

    Hide Answer

    <section class="admin">
      <%= link_to "Edit", edit_movie_path(@movie), class: "button" %>
    </section>
    
  5. Finally, hop back into your browser, navigate to a movie's show page, and click the newly-generated "Edit" link. You should get the following error:

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

    Clearly we need to implement the edit action next.

    Before moving to the next step, think about how you'll do it! It's good to start internalizing the high-level steps before jumping right to the code.

2. Create the Edit Action and Form

Did you figure it out? The edit action needs to do two things. First, similar to the show action, the edit action needs to use the ID embedded in the URL to find the appropriate movie in the database. Then the action needs to render a form and populate the form fields with the respective movie's attributes.

  1. First up, define an edit action in the MoviesController that finds the requested movie and assigns it to an instance variable named @movie.

    Remember that because the route has a variable named :id, you can get the movie id embedded in the URL by accessing params[:id] from within the edit action.

    def edit
      @movie = Movie.find(params[:id])
    end
    
  2. Refresh the page (you're still accessing http://localhost:3000/movies/1/edit) and you should totally expect this error:

    No template for interactive request
    
    MoviesController#edit is missing a template...
  3. Following the error, create a file named edit.html.erb in the app/views/movies directory. Inside that file, start by simply displaying the movie's title as a quick sanity check that the correct movie is being fetched from the database.

    Show Answer

    Hide Answer

    <h1>Editing <%= @movie.title %></h1>
    

    Refresh and you should see the movie title. So far, so good!

  4. Now that we have a movie object in the template, let's start incrementally working on the form. First, update the edit.html.erb template to generate a form using the form_with helper method. For now, just add a label and a text field for the movie's title attribute.

    Remember that the form_with helper needs to be called with the model: option set to the @movie object. Think of it as "binding" the form to the movie. And to disable submitting the form using an Ajax/JavaScript request, set the local: option to true. Then, inside the form_with block, you can use the block parameter (typically named f) which is a form builder to call helpers that generate HTML form elements. For example, f.text_field :title generates a text field for the movie's title attribute.

    <%= form_with(model: @movie, local: true) do |f| %>
      <%= f.label :title %>
      <%= f.text_field :title %>
    <% end %>
    

    Refresh and you should see a label and a text field populated with the the movie's title.

  5. Then incrementally add form elements so you can edit a movie's description, rating, released on date, and total gross. You'll need to generate the appropriate HTML form element depending on the type of attribute:

    • description (a text attribute) goes in a text area with 7 rows, for example
    • rating (a string attribute) goes in a text field
    • released_on (a date attribute) goes in a date select with the HTML option class set to "date".
    • total_gross (a decimal attribute) goes in a number field

    As you work through these you might find it helpful to use the documentation at http://api.rubyonrails.org/. For example, search for "text_field" and "date_select" to find the corresponding form helpers.

    Show Answer

    Hide Answer

    <%= form_with(model: @movie, local: true) do |f| %>
    
      <%= f.label :title %>
      <%= f.text_field :title %>
    
      <%= f.label :description %>
      <%= f.text_area :description, rows: 7 %>
    
      <%= f.label :rating %>
      <%= f.text_field :rating %>
    
      <%= f.label :released_on %>
      <%= f.date_select :released_on, {}, {class: "date"} %>
    
      <%= f.label :total_gross %>
      <%= f.number_field :total_gross %>
    <% end %>
    
  6. When you're done, refresh the form and all the fields should contain the values for the attributes in the @movie object. In other words, the values in the form should reflect the movie details that are in the database.

  7. Finally, add a submit button at the bottom of the form:

    Show Answer

    Hide Answer

    <%= form_with(model: @movie, local: true) do |f| %>
      <%= f.label :title %>
      <%= f.text_field :title %>
    
      <%= f.label :description %>
      <%= f.text_area :description, rows: 7 %>
    
      <%= f.label :rating %>
      <%= f.text_field :rating %>
    
      <%= f.label :released_on %>
      <%= f.date_select :released_on, {}, {class: "date"} %>
    
      <%= f.label :total_gross %>
      <%= f.number_field :total_gross %>
    
      <%= f.submit %>
    <% end %>
    

    Refresh and you should see an "Update Movie" button. Wait a minute! How did Rails know that the submit button should say "Update Movie"? It figured that out because the @movie object you passed to form_with is an existing record in the database, so you must be trying to update it. Pretty clever, eh?

  8. Bonus: As an alternative to using the date_select form helper, you can instead use the date_field helper. This helper creates an input of type "date", which in the Chrome browser will show a little triangle next to the input box. Click the triangle, and a calendar pops up so you can easily pick a date! Note that not all browsers support the HTML 5 "date" type.

    Show Answer

    Hide Answer

    <%= f.date_field :released_on %>
    

It's tempting to want to submit the form, but that's the challenge of the next exercise. So if you're seeing a form populated with a movie's data, then you're good to go for this exercise!

Solution

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

Wrap Up

Excellent! Now we're showing a form for changing a movie's details. Next we need to handle what happens when a user submit the form...