Skip to content

Latest commit

 

History

History
272 lines (222 loc) · 12.1 KB

scaffolding.md

File metadata and controls

272 lines (222 loc) · 12.1 KB

Step 3 — Scaffolding the Application

To create our library application, we will need to create a model to manage our application data, views to enable user interaction with that data, and a controller to manage communication between the model and the views. To build these things we will use the rails generate scaffold command, which will give us a model, a database migration to alter the database schema, a controller, a full set of views to manage Create, Read, Update, and Delete (CRUD) operations for the application, and templates for partials, helpers, and tests.

Because the generate scaffold command does so much work for us, we'll take a closer look at the resources it creates to understand the work that Rails is doing under the hood.

Our generate scaffold command will include the name of our model and the fields we want in our database table. Rails uses Active Record to manage relationships between application data, constructed as objects with models, and the application database. Each of our models is a Ruby class, while also inheriting from the ActiveRecord::Base class. This means that we can work with our model class in the same way that we would work with a Ruby class, while also pulling in methods from Active Record. Active Record will then ensure that each class is mapped to a table in our database, and each instance of that class to a row in that table.

Type the following command to generate a Book model, controller, and associated views:

dip rails generate scaffold Book title:string description:text price:float

With title:string, price:float, and description:text we are giving Rails information about the fields we would like in our database table and the type of data they should accept.

When you type this command, you will again see a long list of output that explains everything Rails is generating for you. The output below highlights some of the more significant things for our setup:

# Output
invoke  active_record
create    db/migrate/20210318152156_create_books.rb
create    app/models/book.rb
invoke    test_unit
create      test/models/book_test.rb
create      test/fixtures/books.yml
invoke  resource_route
route    resources :books
invoke  scaffold_controller
create    app/controllers/books_controller.rb
invoke    erb
create      app/views/books
create      app/views/books/index.html.erb
create      app/views/books/edit.html.erb
create      app/views/books/show.html.erb
create      app/views/books/new.html.erb
create      app/views/books/_form.html.erb
invoke    resource_route
invoke    test_unit
create      test/controllers/books_controller_test.rb
create      test/system/books_test.rb
invoke    helper
create      app/helpers/books_helper.rb
invoke      test_unit
invoke    jbuilder
create      app/views/books/index.json.jbuilder
create      app/views/books/show.json.jbuilder
create      app/views/books/_book.json.jbuilder
invoke  assets
invoke    scss
create      app/assets/stylesheets/books.scss
invoke  scss
create    app/assets/stylesheets/scaffolds.scss

Rails has created the model at app/models/book.rb and a database migration to go with it: db/migrate/20210318152156_create_books.rb. The timestamp on your migration file will differ from what you see here. You can see the fields we included in our initial call of rails generate scaffold. Let's update the title column with a few constraints.

db/migrate/20210318152156_create_books.rb
create_table :books do |t|
  t.string :title, null: false
  t.text :description
  t.float :price

  t.timestamps
end

null: false specifies that a bookrecord must have a title in order to be stored in the database. Consider it an extra layer of validation. To create the Book table run

dip rails db:migrate

It has also created a controller, app/controllers/book_controller.rb , as well as the views associated with our application's CRUD operations, collected under app/views/books. Among these views is a partial, _form.html.erb, that contains code used across views.

Finally, Rails added a new resourceful route, resources :books, to config/routes.rb. This enables the Rails router to match incoming HTTP requests with the books controller and its associated views.

Though Rails has done much of the work of building out our application code for us, it is worth taking a look at some files to understand what is happening.

First, let's look at the controller file within VSCode:

app/controllers/books_controller.rb
class BooksController < ApplicationController
  before_action :set_book, only: %i[ show edit update destroy ]

  # GET /books or /books.json
  def index
    @books = Book.all
  end

  # GET /books/1 or /books/1.json
  def show
  end

  # GET /books/new
  def new
    @book = Book.new
  end

  # GET /books/1/edit
  def edit
  end

  # POST /books or /books.json
  def create
    @book = Book.new(book_params)

    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: "Book was successfully created." }
        format.json { render :show, status: :created, location: @book }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /books/1 or /books/1.json
  def update
    respond_to do |format|
      if @book.update(book_params)
        format.html { redirect_to @book, notice: "Book was successfully updated." }
        format.json { render :show, status: :ok, location: @book }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /books/1 or /books/1.json
  def destroy
    @book.destroy
    respond_to do |format|
      format.html { redirect_to books_url, notice: "Book was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_book
      @book = Book.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def book_params
      params.fetch(:book, {})
    end
end

The controller is responsible for managing how information gets fetched and passed to its associated model, and how it gets associated with particular views. As you can see, our books controller includes a series of methods that map roughly to standard CRUD operations. However, there are more methods than CRUD functions, to enable efficiency in the case of errors.

For example, consider the create method:

 .. .
  def create
    @book = Book.new(book_params)

    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: "Book was successfully created." }
        format.json { render :show, status: :created, location: @book }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end
.. .

If a new instance of the Book class is successfully saved, redirect_to will spawn a new request that is then directed to the controller. This will be a GET request, and it will be handled by the show method, which will show the user the book they've just added.

If there is a failure, then Rails will render the app/views/books/new.html.erb template again rather than making another request to the router, giving users another chance to submit their data.

In addition to the books controller, Rails has given us a template for an index view, which maps to the index method in our controller. We will use this as the root view for our application, so it's worth taking a look at it within VSCode.

app/views/books/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Books</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Description</th>
      <th>Price</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @books.each do |book| %>
      <tr>
        <td><%= book.title %></td>
        <td><%= book.description %></td>
        <td><%= book.price %></td>
        <td><%= link_to 'Show', book %></td>
        <td><%= link_to 'Edit', edit_book_path(book) %></td>
        <td><%= link_to 'Destroy', book, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Book', new_book_path %>

The index view iterates through the instances of our Book class, which have been mapped to the books table in our database. Using ERB templating, the view outputs each field from the table that is associated with an individual book instance: title, description, and price.

The view then uses the link_to helper to create a hyperlink, with the provided string as the text for the link and the provided path as the destination. The paths themselves are made possible through the helpers that became available to us when we defined the books resourceful route with the rails generate scaffold command. In addition to looking at our index view, we can also take a look at the new view to see how Rails uses partials in views. Let's view the app/views/books/new.html.erb template within VSCode:

app/views/books/new.html.erb
<h1>New Book</h1>

<%= render 'form', book: @book %>

<%= link_to 'Back', books_path %>

Though this template may look like it lacks input fields for a new book entry, the reference to render 'form' tells us that the template is pulling in the _form.html.erb partial, which extracts code that is repeated across views.

Looking at that file within VSCode will give us a full sense of how a new book instance gets created:

app/views/books/_form.html.erb
<%= form_with(model: book) do |form| %>
  <% if book.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(book.errors.count, "error") %> prohibited this book from being saved:</h2>

      <ul>
        <% book.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :description %>
    <%= form.text_area :description %>
  </div>

  <div class="field">
    <%= form.label :price %>
    <%= form.text_field :price %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

This template makes use of the form_with form helper. Form helpers are designed to facilitate the creation of new objects from user input using the fields and scope of particular models. Here, form_with takes model: book as an argument, and the new form builder object that it creates has field inputs that correspond to the fields in the books table. Thus users have form fields to enter a book title, description, and price.

Submitting this form will create a JSON response with user data that the rest of your application can access by way of the params method, which creates a ActionController::Parameters object with that data.

Now that you know what rails generate scaffold has produced for you, you can move on to setting the root view for your application.