Forms: Creating Records

Exercises

Objective

At this point we can edit movies in the web interface, but we have no way to create new movies outside of the console. That hardly makes for a useful web app, so let's fix that!

It turns out there's a lot of symmetry between editing movies and creating movies, so we'll follow similar steps:

  1. Add a route to handle requests for /movies/new to show a form.

  2. Generate an "Add New Movie" link on the index page.

  3. Define a new action in the MoviesController that renders a form template.

  4. Create a new.html.erb template that generates an HTML form with blank fields

  5. Define a create action in the MoviesController that inserts the movie into the database when the form is submitted

Visually, here's what we want to do first:

The good news is there's honestly not much new to learn. We already have a form, and we've learned how to process form data in an action. So we'll pick up the pace a bit...

1. Add a Route and Link

As usual, let's start at a high level with the URL. By convention in Rails, the URL for creating a new movie would be http://localhost:3000/movies/new. And that request would get sent to a new action because we want to show the form for creating a new movie. We'll put an "Add New Movie" link on the index page to make it easy to get to the form.

  1. Browse to http://localhost:3000/movies/new and you'll get this brand spanking new error:

    ActiveRecord::RecordNotFound in MoviesController#show
    
    Couldn't find Movie with 'id'=new

    Whoa! That's not what we expected. The router recognizes the request and sends it to the show action. What's up with that?

  2. In these cases, your go-to debugging tool is http://localhost:3000/rails/info/routes. It never lies. You'll see that we currently have these five routes defined:

    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
    

    According to those routes, a GET request for movies/new would map to the show action in the MoviesController. Why? Well, it thinks the word "new" should be used to fill in the :id placeholder.

  3. So our first step would be to define a new route that matches the literal movies/new and sends it to the new action. Because routes are evaluated top to bottom for a match, we'd need to put the movies/new route before the movies/:id route, like so:

    Rails.application.routes.draw
      root   "movies#index"
      get   "movies"     => "movies#index"
      get   "movies/new" => "movies#new"
      get   "movies/:id" => "movies#show", as: "movie"
      get   "movies/:id/edit" => "movies#edit", as: "edit_movie"
      patch "movies/:id" => "movies#update"
    end
    

    But defining these routes one-by-one is getting kinda tedious. Rails to the rescue! It's common to want routes for all these scenarios, plus a few more, on other entities in your application. Rails calls these entities resources. And to eliminate some of the grunt work, Rails has a handy convention for defining all the routes a resource might need in one fell swoop.

  4. Open the config/routes.rb file and replace all the routes we defined previously (except the root route) with the highlighted line below:

    Rails.application.routes.draw
      root "movies#index"
      resources :movies
    end
    
  5. Then reload http://localhost:3000/rails/info/routes to see what that does for us. You should see nine total routes: the five routes we had before plus four new routes highlighted below:

    Helper         HTTP Verb        Path                  Controller#Action
    root_path         GET        /
    movies_path       GET        /movies(.:format)           movies#index
                      POST      /movies(.:format)         movies#create
    new_movie_path  GET        /movies/new(.:format)    movies#new
    edit_movie_path   GET        /movies/:id/edit(.:format)  movies#edit
    movie_path        GET        /movies/:id(.:format)       movies#show
                      PUT       /movies/:id(.:format)     movies#update
                      PATCH      /movies/:id(.:format)       movies#update
                      DELETE    /movies/:id(.:format)     movies#destroy
    

    Magic? No, the resources :movies line simply used the built-in Rails conventions to dynamically define all those routes. And because we used the Rails conventions when defining the routes manually, we end up with the same routes plus a couple extras. Even the route names are the same as the names we picked. And that means our application should work exactly as it did before! It's almost like we planned it that way...

    So we don't need to define any new routes after all. Notice that routes for GET requests to /movies/new and POST requests to /movies are already defined. And that's exactly what the doctor ordered for this task!

  6. Given those routes, use a route helper method to generate an "Add New Movie" link at the bottom of the index page. 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 "Add New Movie", new_movie_path, class: "button" %>
    </section>
    
  7. Back in your browser, navigate to the movie listing page and click the newly-generated "Add New Movie" link. You should get the following error:

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

    Now we're off to the races. Think you know the steps to get us to the finish line? Give it some thought before moving on to the implementation in the next step.

2. Create the New Action and Form

The new action needs to render a form so we can enter a new movie's information. This is similar to the edit action, but in this case we don't have an existing movie in the database. So the new action needs to instantiate a new Movie object in memory, so that the form can "bind" to that object.

  1. Define a new action in the MoviesController that instantiates a new (empty) movie object and assigns it to an instance variable named @movie.

    Show Answer

    Hide Answer

    def new
      @movie = Movie.new
    end
    
  2. Refresh the page (you're still accessing http://localhost:3000/movies/new) and by now you should be so comfortable with this error that resolving it is as easy as falling off a log:

    No template for interactive request
    
    MoviesController#new is missing a template...
  3. Create a file named new.html.erb in the app/views/movies directory. Inside that file, just add enough to get some text on the page:

    <h1>Create a New Movie</h1>
    

    Refresh as a blazing-fast smoke test.

  4. Finally, we need to generate a form in the new.html.erb template that uses the @movie object. It turns out that form would be exactly the same as the form we already have in the edit.html.erb template. So for now, copy the form that's in edit.html.erb and paste it into the new.html.erb template. (Don't worry: we'll remove this duplication later.)

    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 %>
    

    When you're done, refresh the page and the form should be displayed with all the fields blank. That seems reasonable given that all the attributes in the @movie object are nil. It's a new object! (Remember, we called Movie.new in the new action.)

    Did you notice that the submit button says "Create Movie"? The form_with helper knows that the @movie object doesn't yet exist in the database, so you must be trying to create it.

Excellent! We've put up a form for creating new movies. Now on to handling what happens when you submit the form...

3. Implement the Create Action

Our next task is to arrange things in our app so that it creates the movie in the database when the form is submitted. Here's what we need to do, visually:

We're feeling pretty cavalier, so let's just see what happens when we submit the form...

  1. Enter a movie title and submit the form by clicking the "Create Movie" button and you should see the following error:

    Unknown action
    
    The action 'create' could not be found for MoviesController
  2. Implement the create action in the MoviesController. To do that, you'll need to do three things. Remember the steps?

    1. Initialize a new Movie object using the submitted form data and assign it to a @movie instance variable

    2. Save the movie to the database

    3. Redirect to the movie's show page

    Recall that the new 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. Put those two things together, and you can instantiate a new Movie object that's primed with the form data. Once you've initialized the movie, save it to the database and use the redirect_to helper to tell the browser to send a new request for the movie's show page.

    def create
      @movie = Movie.new(params[:movie])
      @movie.save
      redirect_to @movie
    end
    
  3. Now enter a movie title and submit the form again. KABOOM!

    ActiveModel::ForbiddenAttributesError

    Oh hello, our nemesis. Again, we're trying to mass assign attributes using form data and Rails steps in to prevent potentially bad things from happening. Just like with the update action, you'll need create a hash of permitted attributes and pass that hash as a parameter to the new method. (Feel free to copy the permit line from the update action for now. )

    Show Answer

    Hide Answer

    def create
      movie_params =
        params.require(:movie).
          permit(:title, :description, :rating, :released_on, :total_gross)
    
      @movie = Movie.new(movie_params)
      @movie.save
      redirect_to @movie
    end
    
  4. Finally, fill out the form for a new movie, preferably one that involves superheroes saving the world from villains. Submit it, and this time the movie should get inserted in the database and you should get redirected to the show page where the new movie is displayed.

BOOM! Now we can edit and create movies in the web interface!

4. Refactor

Whoa—hold up there a minute, pardner! We're not quite ready to declare victory. Have a look in your update and create actions and you'll see a wee bit of duplication:

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

This sort of unnecessary duplication is what makes apps hard to change. Suppose, for example, that down the road we decide to add new movie attributes and allow them to be mass assigned. We'd have to remember to change the app in two places: in the update action and the create action.

Instead, we'd like to be able to reuse the same list of permitted attributes between those actions. To do that, we'll create a method that returns the permitted attribute list, then call that method from both the update and create actions. By encapsulating the permit list inside of a method, we can add (or remove) permitted attributes later simply by changing that single method.

  1. Inside of the MoviesController class, define a private method called movie_params that returns a list of permitted parameters. (The name of the method is arbitrary.) Private methods can't be called from outside of the class, which means private actions aren't treated as web-accessible actions.

    You specify that a method should be private by calling the private method on a line by itself within a class definition. Then any methods defined after that point (or until a new access level is set) will be private.

    class MoviesController < ApplicationController
    
      # existing public methods (actions)
    
    private
    
      def movie_params
        params.require(:movie).
          permit(:title, :description, :rating, :released_on, :total_gross)
      end
    
    end
    
  2. Then change the update and create actions to call your new movie_params method to get the permitted parameters, rather than explicitly creating a movie_params variable that points to a parameter list.

    Show Answer

    Hide Answer

    def update
      @movie = Movie.find(params[:id])
      @movie.update(movie_params)
      redirect_to @movie
    end
    
    def create
      @movie = Movie.new(movie_params)
      @movie.save
      redirect_to @movie
    end
    
  3. Finally, as a quick test, edit or create a movie. Since we've only refactored code, and not changed any functionality, everything should still work.

Solution

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

Bonus Round

Where Did POST Come From?

When we submitted the form, the form data was sent to the create action. But how did Rails know to do that? Simply put, because that's what the routes dictate.

View the page source of the form and focus in on this part of the HTML form:

<form action="/movies" method="post">
  ...
</form>

The action says that submitting this form will send the data to /movies. The method says that the HTTP verb will be a POST. Rails doesn't override this with a hidden field. So submitting the form will send the form data in a POST request to /movies.

What do the defined routes have to say about that?

Helper         HTTP Verb        Path                  Controller#Action
root_path         GET        /
movies_path       GET        /movies(.:format)           movies#index
                  POST       /movies(.:format)           movies#create
new_movie_path    GET        /movies/new(.:format)       movies#new
edit_movie_path   GET        /movies/:id/edit(.:format)  movies#edit
movie_path        GET        /movies/:id(.:format)       movies#show
                  PUT        /movies/:id(.:format)       movies#update
                  PATCH      /movies/:id(.:format)       movies#update
                  DELETE     /movies/:id(.:format)       movies#destroy

Notice there's a route for the verb GET and the URL /movies and a route for the verb POST and the URL /movies (highlighted). Given the unique combination of HTTP verb and URL, according to the routes POST requests for /movies are handled by the create action in the controller. It's that simple!

Wrap Up

Now that you're getting good with forms and their conventions, it's time for some celebration! Do your victory dance, give a couple fist pumps, or (if those options just seem too crazy) check your Twitter or Slack stream. Between editing movies and creating movies, you've learned a ton!

Up to this point we've implemented all the routes except delete, which we'll get to shortly. In the next section, we'll use partials to remove the duplication we created when we copy/pasted the HTML form code between the edit and new pages.