Showing Detailed Information in the Browser

Your bookstore is starting to come along. You have an index that lists all the books in your database by title and author. This is great, but what about the other information you’ve stored about your books? It would be useful to see how much a book costs, or how many are available.

Let’s show this information.

Let’s start by looking at your application’s routes again.

  1. Open Terminal and go to your bookstore directory.

  2. Run rake routes.

    So far we’ve worked with the first row in the routes table. We used the /books path with the BooksController index action to list all your application’s books.

    books GET    /books(.:format)          books#index
    

    Now that we want to show detailed book information we’ll need to use a different route.

    book GET    /books/:id(.:format)      books#show
    

    This time, we’ll use the /books/:id path with the BooksController show action to show a book’s information.

  $ pwd
  /Users/awesomesauce/Projects/bookstore

  $ rake routes
     Prefix Verb   URI Pattern               Controller#Action
      books GET    /books(.:format)          books#index
            POST   /books(.:format)          books#create
   new_book GET    /books/new(.:format)      books#new
  edit_book GET    /books/:id/edit(.:format) books#edit
       book GET    /books/:id(.:format)      books#show
            PATCH  /books/:id(.:format)      books#update
            PUT    /books/:id(.:format)      books#update
            DELETE /books/:id(.:format)      books#destroy

In the /books/:id path, we can take advantage of :id to get a book by it’s id.

What’s that? You didn’t know a book could have an id? Let’s take a look.

  1. Go back to Terminal and start rails console.

  2. In the console, run Book.first to get the first book from your database.

    Take a look at the output. It might be hard to see, but at the beginning there’s an id.

    #<Book id: 1, title: "why's (poignant) Guide to Ruby", ... >
    

    The first book in your database has an id of 1.

  3. I wonder what id your second book will have…

    Run Book.second and look at the output.

    #<Book id: 2, title: "Oh, the Places You'll Go!", ... >
    

    The second book in your database has an id of … 2!!!

    By default, every book you create will be given a unique id, and it will be given an id in sequential order.

  4. Exit the console.

  $ rails c
  Loading development environment (Rails 5.0.0.1)
  >> Book.first
    Book Load (0.2ms)  SELECT  "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ?  [["LIMIT", 1]]
  => #<Book id: 1, title: "why's (poignant) Guide to Ruby", author: "why the lucky stiff", price_cents: 100, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-26 15:51:15", quantity: 500>

  >> Book.second
    Book Load (0.2ms)  SELECT  "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
  => #<Book id: 2, title: "Oh, the Places You'll Go!", author: "Dr. Seuss", price_cents: 500, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-26 15:51:15", quantity: 200>

  >> exit

How will the /books/:id path work in action?

  1. Go back to Terminal and start your application’s web server by running rails server.

  2. :id can be replaced with specific ids. Since we know your first book’s id is 1, we can go to /books/1.

    Open your browser and go to http://localhost:3000/books/1.

  3. An unknown action error?! Why does that look familiar… 😉

    You’re trying to access the BooksController show action, but it doesn’t exist. Let’s make it!

  $ rails server
  => Booting Puma
  => Rails 5.0.0.1 application starting in development on http://localhost:3000
  => Run `rails server -h` for more startup options
  Puma starting in single mode...
  * Version 3.6.0 (ruby 2.3.1-p112), codename: Sleepy Sunday Serenity
  * Min threads: 5, max threads: 5
  * Environment: development
  * Listening on tcp://localhost:3000
  Use Ctrl-C to stop
  Started GET "/books/1" for ::1 at 2016-12-26 12:07:18 -0500
    ActiveRecord::SchemaMigration Load (0.2ms)  SELECT "schema_migrations".* FROM "schema_migrations"

  AbstractController::ActionNotFound (The action 'show' could not be found for BooksController):

  ...

Browser showing Unknown action error: "The action 'show' could not be found for BooksController"

  1. Open your text editor and go to app/controllers/books_controller.rb.

  2. After the index action, add the following:

    def show
    end
    
  3. Save your changes, and go to http://localhost:3000/books/1 again.

  4. Now you get a different error:

    ActionController::UnknownFormat (BooksController#show is missing a template for this request format and variant.
    

    We’ve seen this error too. You added the show action, but it doesn’t have a template to go with it.

1
2
3
4
5
6
7
8
  class BooksController < ApplicationController
    def index
      @books = Book.all
    end

    def show
    end
  end

Browser showing ActionController::UnknownFormat error: "BooksController#show is missing a template for this request format and variant"

Let’s add the missing template.

  1. Go back to Terminal and stop your application’s web server by running Ctrl-C.

  2. Now, run touch app/views/books/show.html.erb to create the empty template.

  3. Restart your application’s web server by running rails server and revisit http://localhost:3000/books/1.

    Remember, a blank page means no errors. 😆

    Now that we have the BooksController show action rendering a template, we’re ready to show a book’s details.

  $ rails server

  ...

  ^CExiting

  $ touch app/views/books/show.html.erb

  $ rails server

  ...

How do we know which book we’re showing?

Before you can show a book’s details, you have to figure out which book’s details to show. How can we figure out which book to show?

When you visit http://localhost:3000/books/1, you send a request to your application’s web server. Thanks to Rails routing, the book id (1) is included in that request.

Started GET "/books/1" for ::1 at 2016-12-26 16:12:07 -0500
Processing by BooksController#show as HTML
  Parameters: {"id"=>"1"}
  Rendering books/show.html.erb within layouts/application
  Rendered books/show.html.erb within layouts/application (0.3ms)
Completed 200 OK in 266ms (Views: 263.1ms | ActiveRecord: 0.0ms)

It’s tucked in there, but the id is inside the parameters hash. See it? Try to find it in your server’s output.

Parameters: {"id"=>"1"}

Let’s see how we can access the parameters hash in your BooksController show method.

  1. Open your text editor and go to app/controllers/books_controller.rb.

  2. Request parameters are available in all your application’s controllers as params.

    Inside the show method, add the following code:

    @id = params[:id]
    
1
2
3
4
5
6
7
8
9
  class BooksController < ApplicationController
    def index
      @books = Book.all
    end

    def show
      @id = params[:id]
    end
  end

By defining @id in the BooksController show method, it’s available in the BooksController show template.

  1. Open app/views/books/show.html.erb in your text editor and add the following:

    This book's id is <%= @id %>.
    
  2. Save your changes.

1
  This book's id is <%= @id %>.

Now that the BooksController show template is wired up to render something, what do you expect to see if you visit http://localhost:3000/books/1?

Do you have an idea? Let’s verify it!

  1. Go to http://localhost:3000/books/1.

    What do you see?? Was it what you were expecting?

Browser showing "/books/1" with the request book's id

Did reality meet your expecations?

The page renders “This book’s id is 1.” becuase you made a request from your browser with book id 1 (http://localhost:3000/books/1).

You’re able to render the id because you grabbed it from the request parameters and assigned it to @id inside the BooksController show action.

What happens if you go to http://localhost:3000/books/2?

What about http://localhost:3000/books/3?

The new book id is rendered on every request because you wired up the template to show the requested book’s id.

Finding a Book

Now that we have the book id for the book we want to show, we can use it to get that book from your application’s database.

We’ve used a few methods to get books from the database. We’ve used Book.all to get all the books, and we’ve used Book.first to get the first book.

But how do we get a book by it’s id? Let’s explore on the rails console.

  1. Go back to Terminal and quit your application’s web server by running Ctrl-C. Then, start the rails console.

  2. Now try running the following code:

    Book.find(3)
    

    What did it return? It returned the book in your application’s database with id 3.

    #<Book id: 3, title: "1984", author: "George Orwell", price_cents: 0, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-26 15:51:15", quantity: 200>
    
  3. What do you get when you run Book.find(1) or Book.find(5)?

    You get the book with that id.

  4. What happens if you do Book.find(100)?

    ActiveRecord::RecordNotFound: Couldn't find Book with 'id'=100
    

    An error! Book.find returns an error when it doesn’t find a book with the given book id.

    So we can use Book.find to get books from your application’s database with a given id. If a book doesn’t exist with that id, we get an ActiveRecord::RecordNotFound error.

  5. Exit the console and restart your application’s web server.

  $ rails server

  ...
  ^CExiting

  $ rails console
  Loading development environment (Rails 5.0.0.1)
  >> Book.find(3)
    Book Load (0.2ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  => #<Book id: 3, title: "1984", author: "George Orwell", price_cents: 0, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-26 15:51:15", quantity: 200>

  >> Book.find(1)
    Book Load (0.2ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  => #<Book id: 1, title: "why's (poignant) Guide to Ruby", author: "why the lucky stiff", price_cents: 100, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-26 15:51:15", quantity: 500>

  >> Book.find(5)
    Book Load (0.2ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
  => #<Book id: 5, title: "Life of Pi", author: "Yann Martel", price_cents: 750, created_at: "2016-12-26 15:51:15", updated_at: "2016-12-26 15:51:15", quantity: 50>

  >> Book.find(100)
    Book Load (0.2ms)  SELECT  "books".* FROM "books" WHERE "books"."id" = ? LIMIT ?  [["id", 100], ["LIMIT", 1]]
  ActiveRecord::RecordNotFound: Couldn't find Book with 'id'=100
	from /Users/alimi/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.0.1/lib/active_record/core.rb:173:in `find'
	from (irb):5
	from /Users/alimi/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0.1/lib/rails/commands/console.rb:65:in `start'
	from /Users/alimi/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0.1/lib/rails/commands/console_helper.rb:9:in `start'
	from /Users/alimi/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0.1/lib/rails/commands/commands_tasks.rb:78:in `console'
	from /Users/alimi/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0.1/lib/rails/commands/commands_tasks.rb:49:in `run_command!'
	from /Users/alimi/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0.1/lib/rails/commands.rb:18:in `<top (required)>'
	from bin/rails:4:in `require'
	from bin/rails:4:in `<main>'

  >> exit

  $ rails server
  => Booting Puma
  ...

Let’s use our new knowledge of Book.find to get and show the requested book in the BooksController show method.

  1. In your text editor, open the BooksController.

  2. We’ve already grabbed the requested book id and set it to @id. Let’s use that to find the requested book.

    Add the following line to the end of the show method:

    @book = Book.find(@id)
    
  3. Save your changes and go to http://localhost:3000/books/1.

    Did you notice anything different? I hope not, because we didn’t update the template 😝

1
2
3
4
5
6
7
8
9
10
  class BooksController < ApplicationController
    def index
      @books = Book.all
    end

    def show
      @id = params[:id]
      @book = Book.find(@id)
    end
  end
  1. Now, open the BooksController show template (app/views/books/show.html.erb).

  2. Add the following line to the end of the file

    This book is called <%= @book.title %>
    
  3. Save your changes and go back to http://localhost:3000/books/1.

1
2
  This book's id is <%= @id %>.
  This book is called <%= @book.title %>.

Browser showing the first book's id and title

Yay! You got the request book and showed some of its information.

Unfortunately, it’s kinda smashed together on the page.

Let’s use a definition list to clean it up.

  1. Open app/views/books/show.html.erb in your text editor and delete everything in the file.

    Yes, EVERYTHING.

    There’s nothing wrong with what we’ve done so far, but adding the definition list is easier from scratch.

  2. Now, add the following code to the file:

    <dl>
      <dt>Id</dt>
      <dd><%= @book.id %></dd>
    
      <dt>Title</dt>
      <dd><%= @book.title %></dd>
    </dl>
    

    This is an HTML definition list with a couple of terms: “Id” and “Title”. The terms are defined with values for the given book.

    Save your changes and go to http://localhost:3000/books/1 to see the results.

1
2
3
4
5
6
7
  <dl>
    <dt>Id</dt>
    <dd><%= @book.id %></dd>

    <dt>Title</dt>
    <dd><%= @book.title %></dd>
  </dl>

Browser show "/books/1" as a definition list

  1. Now that we have a definition list with the book's id and title, update it to include the rest of the book's information (author, price_cents, and quantity).

  1. What did you come up with? Your solution should look something like this:

    <dl>
      <dt>Id</dt>
      <dd><%= @book.id %></dd>
    
      <dt>Title</dt>
      <dd><%= @book.title %></dd>
    
      <dt>Author</dt>
      <dd><%= @book.author %></dd>
    
      <dt>Price Cents</dt>
      <dd><%= @book.price_cents %></dd>
    
      <dt>Quantity</dt>
      <dd><%= @book.quantity %></dd>
    </dl>
    
  2. Update your solution to match this solution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <dl>
    <dt>Id</dt>
    <dd><%= @book.id %></dd>

    <dt>Title</dt>
    <dd><%= @book.title %></dd>

    <dt>Author</dt>
    <dd><%= @book.author %></dd>

    <dt>Price Cents</dt>
    <dd><%= @book.price_cents %></dd>

    <dt>Quantity</dt>
    <dd><%= @book.quantity %></dd>
  </dl>

Price Cents?

The books details look great, but doesn’t “Price Cents” look kinda weird?

Do you ever wonder how much something would cost in pennies? Really?! I thought I was the only one!

For most people, it would be easier to see the book’s price in dollars. Fortunately, Rails has our back.

Rails provides a number_to_currency method that prints a number as currency.

Let’s see what happens when you use it.

  1. Reopen app/views/books/show.html.erb and change the “Price Cents” definition from

    <dd><%= @book.price_cents %></dd>
    

    to

    <dd><%= number_to_currency(@book.price_cents) %>
    
  2. Save your changes and revist http://localhost:3000/books/1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <dl>
    <dt>Id</dt>
    <dd><%= @book.id %></dd>

    <dt>Title</dt>
    <dd><%= @book.title %></dd>

    <dt>Author</dt>
    <dd><%= @book.author %></dd>

    <dt>Price Cents</dt>
    <dd><%= number_to_currency(@book.price_cents) %></dd>

    <dt>Quantity</dt>
    <dd><%= @book.quantity %></dd>
  </dl>

Browser showing "/books/1" with `number_to_currency(price_cents)`

Looks great, right?!

Except why’s (poignant) Guide to Ruby doesn’t cost $100.00. We set the book’s price_cents to 100, and 100 cents is $1.00.

number_to_currency formats a number as a dollar amount, but the number has to be in dollars.

We need to first convert price_cents to dollars before we can really use number_to_currency.

  1. Reopen app/views/books/show.html.erb and change the “Price Cents” definition from

    <dd><%= number_to_currency(@book.price_cents) %></dd>
    

    to

    <dd><%= number_to_currency(@book.price_cents / 100.0) %></dd>
    

    To convert from cents to dollars, we can simply divide by 100.

    (Actually we have to divide by 100.0 because floating point math is fun)

    Then, the converted value is used by number_to_currency.

  2. You can also go ahead and change the definition term from “Price Cents” to “Price”.

  3. Save your changes and try looking up a few of your books in your browser.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <dl>
    <dt>Id</dt>
    <dd><%= @book.id %></dd>

    <dt>Title</dt>
    <dd><%= @book.title %></dd>

    <dt>Author</dt>
    <dd><%= @book.author %></dd>

    <dt>Price</dt>
    <dd><%= number_to_currency(@book.price_cents / 100.0) %></dd>

    <dt>Quantity</dt>
    <dd><%= @book.quantity %></dd>
  </dl>

Browser showing "/books/1" with a nicely formated price

We’re making really good progress! Anyone can visit your bookstore to see what books you have, and they can learn a little about those books.

I love that I can see what books you have and how much they cost, but what if I’ve never heard of one of your books before??

Let’s add book descriptions to help give just a little more info about your books.

Adding descriptions to books

We can add descriptions to books by adding a description column to the books table.

Do you remember how we added columns to your application’s database tables?

We’ve done that through migrations. We created a migration to add the books table, and we changed that migration to add new columns.

Now that you’re bookstore application is coming along, we don’t want to go back and edit past migrations. Editing past migrations can compromise the data you’ve worked so hard to collect!

But how will we add the description column to the books table??

A new migration!

  1. Go back to Terminal and stop your application’s web server by running Ctrl-C.

  2. Now, run rails generate migration add_description_to_books.

    This creates a new timestamped migration file db/migrate/TIMESTAMP_add_description_to_books.rb.

  $ rails generate migration add_description_to_books
        invoke  active_record
        create    db/migrate/20161227020938_add_description_to_books.rb
  1. Open your new migration in your text editor.

  2. Inside the change method, add the following line:

    add_column :books, :description, :text
    

    With this change, we’re setting up the migration to add a column to the books table. The new column will be named description and it will store any amount of text.

  3. Save your changes.

1
2
3
4
5
  class AddDescriptionToBooks < ActiveRecord::Migration[5.0]
    def change
      add_column :books, :description, :text
    end
  end
  1. Now, go back to Terminal.

    Run rake db:migrate to run your new migration against your application’s database.

  $ rake db:migrate
  == 20161227020938 AddDescriptionToBooks: migrating ============================
  -- add_column(:books, :description, :text)
     -> 0.0049s
  == 20161227020938 AddDescriptionToBooks: migrated (0.0050s) ===================

Let’s take a look at how this changes your bookstore.

  1. In Terminal, start rails console, get your first book and assign it to a variable called my_first_book.

    my_first_book = Book.first
    

    Does the book look different?

    At the end, you should see a nil description.

    #<Book id: 1, title: "why's (poignant) Guide to Ruby", ..., description: nil>
    

    description is nil because you just added it.

  2. You can give my_first_book a description by running

    my_first_book.update(description: "YOUR INSPIRING DESCRIPTION")
    

    Where YOUR INSPIRING DESCRIPTION is…YOUR INSPIRING DESCRIPTION.

    (Don’t worry about your description being inspiring 😉)

  3. After you’ve added YOUR INSPIRING DESCRIPTION to my_first_book, you can run my_first_book.description to see the new description.

  4. Go ahead and add descriptions for the of rest your books. Your application’s database should have seven books, which means you have six descriptions to write.

  5. When you’re done, exit the rails console and restart your application’s web server by running rails server.

  $ rails console
  Loading development environment (Rails 5.0.0.1)

  >> my_first_book = Book.first

  >> my_first_book.update(description: "An introductory book to the Ruby programming language, written by why the lucky stiff. The book is unusual among programming books in that it includes quite a lot of strange humor and narrative side tracks which are sometimes completely unrelated to the topic. Many motifs have become inside jokes in the Ruby community, such as references to the words 'chunky bacon'. The book includes many characters which have become popular as well, particularly the cartoon foxes and Trady Blix, a large black feline friend of why's, who acts as a guide to the foxes and occasionally teaches them some Ruby.")
     (0.1ms)  begin transaction
    SQL (0.6ms)  UPDATE "books" SET "updated_at" = ?, "description" = ? WHERE "books"."id" = ?  [["updated_at", 2016-12-27 02:28:28 UTC], ["description", "An introductory book to the Ruby programming language, written by why the lucky stiff. The book is unusual among programming books in tha    t it includes quite a lot of strange humor and narrative side tracks which are sometimes completely unrelated to the topic. Many motifs have become inside jokes in the Ruby comm    unity, such as references to the words 'chunky bacon'. The book includes many characters which have become popular as well, particularly the cartoon foxes and Trady Blix, a larg    e black feline friend of why's, who acts as a guide to the foxes and occasionally teaches them some Ruby."], ["id", 1]]
     (2.3ms)  commit transaction
  => true

  >> my_first_book.description
  => "An introductory book to the Ruby programming language, written by why the lucky stiff. The book is unusual among programming books in that it includes quite a lot of strange humor and narrative side tracks which are sometimes completely unrelated to the topic. Many motifs have become inside jokes in the Ruby community, such as references to the words 'chunky bacon'. The book includes many characters which have become popular as well, particularly the cartoon foxes and Trady Blix, a large black feline friend of why's, who acts as a guide to the foxes and occasionally teaches them some Ruby."

  ...

  >> exit
  1. Now that your books have descriptions, you can share them with the WORLD!

    Reopen app/views/books/show.html.erb and add the book’s description to the end of the definition list.

  1. What did you come up with? You should’ve added the following lines:

    <dt>Description</dt>
    <dd><%= @book.description %></dd>
    
  2. Save your changes and view your beautiful descriptions in your browser.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  <dl>
    <dt>Id</dt>
    <dd><%= @book.id %></dd>

    <dt>Title</dt>
    <dd><%= @book.title %></dd>

    <dt>Author</dt>
    <dd><%= @book.author %></dd>

    <dt>Price</dt>
    <dd><%= number_to_currency(@book.price_cents / 100.0) %></dd>

    <dt>Quantity</dt>
    <dd><%= @book.quantity %></dd>

    <dt>Description</dt>
    <dd><%= @book.description %></dd>
  </dl>

Browser showing "/books/1" with a lovely description of "why's (poignant) Guide to Ruby"

Now you have a lovely books index and lots of individual book views, but there is no relationship between the two. Let’s link them!

Since you have a books index with a little bit of book information and book views lots of specific information, it makes sense to link from the books index to specific book pages.

If a visitor to your bookstore is really excited to see a book on the index, they could click on that book to get more detailed information about it.

We’ll update the index so book titles link to their book’s page.

In HTML, you add links using anchor elements (<a> tags). Anchor elements link to other pages by their href attribute.

Fortunately, Rails gives us a convience method to abstract away some of that work.

  1. In your text editor, open app/views/books/index.html.erb.

  2. Change the list item that renders book titles and authors to this:

    <li>
      <%= link_to(book.title, book_path(book)) %> by <%= book.author %>
    </li>
    

    Rails gives us a link_to method we can use to create anchor elements.

    First, you give it the text for the link. In this case, we want to use the book’s title as the link text.

    Then, you give it the path for the link. We want the link to go to /books/:id, but we passed it book_path(book). book_path(book) is another Rails method. Long story short, it generates /books/:id for the given book.

  3. Save your changes and go to http://localhost:3000/books. Try clicking all the links!

    When you’re done, stop your application’s web server.

1
2
3
4
5
6
7
8
9
  <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>

Browser showing books index with all the links!