Creating Books Through the Browser
So far you’ve created books on the rails console
. It works, but it’s not the best experience. In most web applications, you enter data through…the web.
Your bookstore is a web application, so let’s treat it like one. Let’s set up your application so you can create books from your browser.
-
Open Terminal and go to the
bookstore
directory. -
Now, run
rake routes
.We’ve worked through a couple of the books routes. First, we used the
index
action to list all your application’s books. Then, we used theshow
action to show details for a given book.Now, we’ll use the
new
action to create a new book. -
The path for the
new
action is/books/new
.new_book GET /books/new(.:format) books#new
Let’s try going to that path.
-
In Terminal, start your application’s web server by running
rails server
. -
Now, try going to http://localhost:3000/books/new.
-
You got an error, but doesn’t it look familiar?
Why you think you got an Unknown action error?
The error message gives you a hint as to why you’re getting an Unknown action error.
The action 'new' could not be found for BooksController
When you make a request to /books/new
, your request gets routed to the BooksController
new
action. What does your BooksController
look like? Does it have a new
action?
The BooksController
doesn’t have a new
action because we haven’t defined it yet.
-
In your text editor, open the
BooksController
(app/controllers/boooks_controller.rb
). -
At the end of the
BooksController
add thenew
method.def new end
-
Save your changes, and revisit http://localhost:3000/books/new.
1
2
3
4
5
6
7
8
9
10
11
12
13
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
end
end
Another error, but it should also look familiar.
BooksController#new is missing a template for this request format and variant.
You’ve added the new
method to the BooksController
, but you haven’t created the new
action’s template.
Let’s add it!
-
Go back to terminal and stop your application’s web server by running
Ctrl-C
. -
Now, run
touch app/views/books/new.html.erb
to create thenew
action’s template. -
Restart your application’s web server and revisit http://localhost:3000/books/new. What do you see?
A blank page! 🎉
A Little About Forms
Data is usually added to web applications through forms.
You probably have never noticed them, but forms are everywhere on the internet. When you login to a site, you enter your credentials into a form. When you post a status on Facebook, you enter your status into a form. When you’re Googling for programming resources, you enter your search terms into a form.
After you add your data to a form, you submit it by clicking a submit button. For example, a login form might have a submit button that says “Login”.
We’re going to use forms to create new books in your application.
-
In your text editor, open
app/views/books/new.html.erb
and add the following code:<%= form_for(@book) do |f| %> <% end %>
form_for
is a method provided by Rails. It provides a consistent interface to build forms inside Rails applications.Here, we’re passing
@book
toform_for
because we want to create a form for a book.form_for
takes a block as its last argument. We haven’t done anything interesting inside the block, but that’ll change soon 😉 -
Save your changes and revisit http://localhost:3000/books/new.
1
2
<%= form_for(@book) do |f| %>
<% end %>
Hmm…an error.
You’re seeing an ArgumentError
in Books#new
. There’s a lot going on in the error, but the message has a helpful hint.
First argument in form cannot contain nil or be empty
Remember the code we used to start the form?
<%= form_for(@book) do |f| %>
@book
is the first argument in the form, so the error is telling us @book
must be nil or empty.
How could @book
be nil?
-
Open the
BooksController
in your text editor and take a look at thenew
method.Is
@book
defined inside in the new method?def new end
It’s nowhere to be found! That explains that error 😅
1
2
3
4
5
6
7
8
9
10
11
12
13
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
end
end
Let’s fix that error.
-
Inside the
BooksController
new
method, add the following line:@book = Book.new
@book
is set to be a new book because we want to create a form for new books. -
Save your changes and revisit http://localhost:3000/books/new.
No errors! But we still have a blank page. Let’s start building out that form.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
@book = Book.new
end
end
-
In your text editor, open
app/views/books.new.html.erb
. -
Inside the
form_for
block, add the following:<ul> <li> <%= f.label :title %> <%= f.text_field :title %> </li> <li> <%= f.label :author %> <%= f.text_field :author %> </li> </ul>
We’re putting your form elements inside an unordered list. The form will have two fields, one for title and one for author.
f.label
defines labels for each field.f.text_field
creates a text input field for each field. -
Save your changes and revisit http://localhost:3000/books/new.
Can you match the code you added to the different elements on the page?
1
2
3
4
5
6
7
8
9
10
11
12
13
<%= form_for(@book) do |f| %>
<ul>
<li>
<%= f.label :title %>
<%= f.text_field :title %>
</li>
<li>
<%= f.label :author %>
<%= f.text_field :author %>
</li>
</ul>
<% end %>
-
Your new book form has fields for title and author.
Try adding a field for the price. Remember, we named the field
price_cents
.
-
What did you come up with? If you followed the pattern used for the other two fields, you probably came up with something like this:
<li> <%= f.label :price_cents %> <%= f.text_field :price_cents %> </li>
That works well, but
price_cents
isn’t a text field. It’s a number.To make it a number field, simply change
<%= f.text_field :price_cents %>
to
<%= f.number_field :price_cents %>
-
Your full solution should look like this:
<li> <%= f.label :price_cents %> <%= f.number_field :price_cents %> </li>
Update your solution, save your changes, and revisit http://localhost:3000/books/new.
Does the
price_cents
field differ from other fields on the page?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul>
<li>
<%= f.label :title %>
<%= f.text_field :title %>
</li>
<li>
<%= f.label :author %>
<%= f.text_field :author %>
</li>
<li>
<%= f.label :price_cents %>
<%= f.number_field :price_cents %>
</li>
</ul>
<% end %>
We have a couple more fields to add to the form. We need fields for quantity and description.
-
Add
quantity
to your form as anumber_field
. -
Add
description
to your form as atext_area
.(
text_area
differs fromtext_field
by offering more space for text - which is useful for long descriptions.)
-
What does your solution look like? Your complete form should look like this:
<%= form_for(@book) do |f| %> <ul> <li> <%= f.label :title %> <%= f.text_field :title %> </li> <li> <%= f.label :author %> <%= f.text_field :author %> </li> <li> <%= f.label :price_cents %> <%= f.number_field :price_cents %> </li> <li> <%= f.label :quantity %> <%= f.number_field :quantity %> </li> <li> <%= f.label :description %> <%= f.text_area :description %> </li> </ul> <% end %>
-
Update your solution to match this solution and save your changes.
Now you have a beautiful form full of fields. You can enter so much book data! 😍
But there’s no way for you to save that data in your application’s database 😞
Don’t worry! We can fix that!
-
At the end of the
form_for
block`, add the following line:<%= f.submit %>
This will add a submission button to your form.
-
Save your changes and revisit http://localhost:3000/books/new.
f.submit
generates a button labeled “Create Book”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%= form_for(@book) do |f| %>
<ul>
<li>
<%= f.label :title %>
<%= f.text_field :title %>
</li>
<li>
<%= f.label :author %>
<%= f.text_field :author %>
</li>
<li>
<%= f.label :price_cents %>
<%= f.number_field :price_cents %>
</li>
</ul>
<%= f.submit %>
<% end %>
-
Now that you have a form with a shiny “Create Book” button, why don’t you try creating a book?
Fill out the form and click “Create Book”.
-
Error? ERROR?!
You’re getting an
Unknown action
error that says “The action ‘create’ could not be found for BooksController”.Huh?
We’ve been working with the
BooksController
new
action? How did we end up with an error for thecreate
action?
Take a look at what happened in your application’s web server when you clicked “Create Book”.
Started POST "/books" for ::1 at 2016-12-27 14:32:50 -0500
AbstractController::ActionNotFound (The action 'create' could not be found for BooksController):
Before the error happened, the server received a POST request to the /books
path. This request happened when you clicked the “Create Book” button.
Your form’s data was submitted as a POST request to the /books
path.
Real interesting, right? Let’s take a look at your application routes again.
-
Go back to Terminal and stop your application’s web server by running
Ctrl-C
. Then, runrake routes
. -
Take a look at the second row of the routing table.
POST /books(.:format) books#create
Does that look familiar? 😉
POST requests to
/books
get sent to theBooksController
create
action!It looks like we were destined to reach the
BooksController
create
action, but how did the form know to go there?By default, a
form_for
with a new record will be wired up to POST requests to its matchingcreate
action.In our case, we setup
form_for
with a new book. Therefore, the form was setup to POST to theBooksController
create
action.Does this make sense to you? If it does, I’m impressed. I’m having a hard time understanding it all. 😅
-
Don’t worry if you don’t understand everything that is going on. It will make more sense over time.
The key take away here is the
new
book form gets submitted to theBooksController
create action. -
Restart your application’s web server.
-
Open the
BooksController
in your text editor. -
Add a
create
method to the end of the controller.def create end
-
Save your changes and try adding a book again.
Nothing happens! Hey, it’s better than an error…
The BooksController
create
action is going to be responsible for using your form data to create a new book.
Take a look at the request parameters that are sent when you submit the new book form.
Started POST "/books" for ::1 at 2016-12-27 15:29:52 -0500
Processing by BooksController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"AOZpk66Fc20R/9xHVhxZLiSwsQj29isG2ohi9gS6TKXT0PUP3n9hYJQrPpY8iSx6uessf9Sgsd3Uv3rEuB/TvQ==", "book"=>{"title"=>"The Cat in the Hat", "author"=>"Dr. Seuss", "price_cents"=>"500", "quantity"=>"1000", "description"=>"That crazy cat!"}, "commit"=>"Create Book"}
Inside the parameters hash, there’s a “book” key with its own hash.
"book"=>{"title"=>"The Cat in the Hat", "author"=>"Dr. Seuss", "price_cents"=>"500", "quantity"=>"1000", "description"=>"That crazy cat!"}
This is the server output for the book I was trying to add, but your server output should look similar.
(Unless you were trying add The Cat in the Hat. Then, it should look pretty much the same…)
To get the book data that’s POSTed to the server, we need to access the book
key in the params
hash.
-
Add the following code to your
BooksController
create
method:Book.create(title: params[:book][:title], author: params[:book][:author], price_cents: params[:book][:price_cents], quantity: params[:book][:quantity], description: params[:book][:description])
We’re using
Book.create
to create a new book. The new book’s attributes are being set with values from theparams
hashbook
key. -
Save you changes and try adding a book again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
@book = Book.new
end
def create
Book.create(title: params[:book][:title], author: params[:book][:author], price_cents: params[:book][:price_cents], quantity: params[:book][:quantity], description: params[:book][:description])
end
end
Nothing happened, right?
Not exactly…
-
Go to Terminal and stop your application’s web server.
-
Now, run
rails console
and get the last book in your database.Does it look familiar?
It’s the book you created!
Although it looked like nothing happened when you clicked submit, the request made it to the
BooksController
create
action andBook.create
was run. -
The
create
action is almost done. Now, we just need make the experience a little better.Exit
rails console
and restart your application’s web server.
-
In your text editor, open the
BooksController
. -
At the end of the
create
method, add the following line:redirect_to books_path
With this change, you’ll be redirected to the
books_path
after a new book is created.books_path
is a Rails helper method. It evaluates to/books
or your book index page. -
Save your changes and revisit http://localhost:3000/books/new.
Add a new book and you will be redirected to the books index. The new book will show up at the end of the index.
Magic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
@book = Book.new
end
def create
Book.create(title: params[:book][:title], author: params[:book][:author], price_cents: params[:book][:price_cents], quantity: params[:book][:quantity], description: params[:book][:description])
redirect_to books_path
end
end
Cleanup
Ah, it feels like we’ve come so far from those days of creating books on the console…
What? It hasn’t been that long?
Anyways…
Things are coming together nicely. Let’s take a look at cleaning some stuff up. First, we’ll start with the BooksController
.
-
Open the
BooksController
in your text editor and take a look at thecreate
method. -
In the first line of the
create
method, you’re callingBook.create
and setting attributes one by one from the params hash.Book.create(title: params[:book][:title], author: params[:book][:author], price_cents: params[:book][:price_cents], quantity: params[:book][:quantity], description: params[:book][:description])
Do you notice a pattern in the attributes that are being set?
Every attribute that you’re setting is in
params[:book]
.title: params[:book][:title] author: params[:book][:author] price_cents: params[:book][:price_cents] ...
Since every attribute you’re setting is in
params[:book]
, we can simplify that line. -
Change
Book.create
fromBook.create(title: params[:book][:title], author: params[:book][:author], price_cents: params[:book][:price_cents], quantity: params[:book][:quantity], description: params[:book][:description])
to
Book.create(params[:book])
-
Save your changes, revisit http://localhost:3000/books/new, and try creating a book.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
@book = Book.new
end
def create
Book.create(params[:book])
redirect_to books_path
end
end
So…that didn’t work.
The change we made is valid, but we’re running into a Rails security feature.
Rails won’t let you set several attributes at once with data from a request. In this case, the data came from a request you generated. However, it’s not hard to imagine a malicious user sending harmful data.
Before you can set several attributes at once from request data, you have to explicitly state which attributes can be set. That’s why you’re seeing an ActiveModel::ForbiddenAttributesError
- you haven’t permitted any attributes.
This security feature is called strong parameters.
-
Reopen your
BooksController
. -
To use strong parameters, change
Book.create
fromBook.create(params[:book])
to
Book.create(params.require(:book).permit(:title, :author, :price_cents, :quantity, :description)
When we want to create a book from the
params
hash, theparams
hash must have abook
hash. From thebook
hash, we’ll get any of the permitted attributes and assign them to the new book. -
Save your changes, revisit http://localhost:3000/books/new, and try creating a book again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BooksController < ApplicationController
def index
@books = Book.all
end
def show
@id = params[:id]
@book = Book.find(@id)
end
def new
@book = Book.new
end
def create
Book.create(params.require(:book).permit(:title, :author, :price_cents, :quantity, :description))
redirect_to books_path
end
end
To create books in the browser, you’ve been going directly to http://localhost:3000/books/new. That works, but it isn’t very convenient.
Let’s add a link to the book index that will take us to http://localhost:3000/books/new.
-
Open
app/view/books/index.html.erb
in your text editor. -
After the unordered list, add the following line:
<%= link_to("Add a book", new_book_path) %>
We’re using the
link_to
helper to create a link. The link’s text will be “Add a book”, and it will link to the new_book_path (/books/new
). -
Save your changes and go to http://localhost:3000/books/new.
You should see the new link. Clicking it should take you to the new book form.
1
2
3
4
5
6
7
8
9
10
11
<h1>Welcome to My Super Rad Bookstore!</h1>
<ul>
<% @books.each do |book| %>
<li>
<%= link_to(book.title, book_path(book)) %> by <%= book.author %>
</li>
<% end %>
</ul>
<%= link_to("Add a book", new_book_path) %>
Now you can add all the books you heart desires from the comfort of your browser.
-
When you’re done adding books and basking in the glory, stop your application’s web server and give yourself a high five.