前回、Scaffoldを使って本を管理する簡単なアプリを作って立ち上げた。
今回は画面の基本的な仕組みを勉強します!
一覧画面の仕組み
http://localhost:3000/booksというURIに基づいて図書一覧が表示されるようになっている。
実際にデータを登録すると、このような表示になる。
前回書いたとおり、ルーターで指定されているresources :books
は、実際には7つのアクションに相当するルートとして実装されている。
そのうちの「GET /books」に対応するルートが「books#index」であるため、このリクエストに基づいてBooksコントローラーのindexアクションが呼ばれる。
# app/controllers/books_controller.rb のindexアクション部分 def index @books = Book.all end
Books.all
は、Bookモデルの持つallメソッドを呼び出すことでBookモデルに相当するデータベーステーブル(books)の全データを取得し、ひとつずつインスタンス化して配列化したものを変数@booksに紐付けている。
indexアクションの最後でビュー出力の指示がない場合、該当するコントローラーのアクション名と同じレビューを探し、それを利用してクライアントレスポンスを返す。つまりviews/booksディレクトリにあるindex.html.erbを呼び出している。
# app/views/books/index.html.erb <p id="notice"><%= notice %></p> <h1>Books</h1> <table> <thead> <tr> <th>Title</th> <th>Description</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @books.each do |book| %> <tr> <td><%= book.title %></td> <td><%= book.description %></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 %>
@booksから1件ずつ図書データのインスタンスをbookとして取り出し、その都度titleとdescriptionを取得している。
新規登録の仕組み
新規登録画面はhttp://localhost:3000/books/newで表示される。
宛先は「GET /books/new」に対応するアクション「books#new」に相当する。
# app/controllers/books_controller.rb のnewアクション部分 def new @book = Book.new end
Book.new
でBookモデルの新規のインスタンスを作成し、そのインスタンスオブジェクトを@bookインスタンス変数に対応付けている。
indexアクションと同様、特にビューへの出力指示がないためviews/booksディレクトリにあるnew.html.erbが呼び出される。
# app/views/books/new.html.erb <h1>New Book</h1> <%= render 'form', book: @book %> <%= link_to 'Back', books_path %>
renderはビューを出力するためのメソッドである。ビューの部品としてビューの中に構成されるビューテンプレートを部分テンプレートという。
部分テンプレート
部分テンプレートは_
ではじまる名前をつけることで他のテンプレートと区別される。記述する時点ではrender 'form'
のように_
を外してformだけで指定するが、実際には_form.html.erbという名前の部分テンプレートに対応している。
# app/views/books/_form.html.erb <%= form_with(model: book, local: true) 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.full_messages.each do |message| %> <li><%= 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="actions"> <%= form.submit %> </div> <% end %>
部分テンプレートを利用すると、新規登録で入力を行う画面と編集で入力する画面とを同じ形式として共通化することができる。
編集画面を表示するedit.html.erbでも_form.html.erbを呼び出している。
# app/views/books/edit.html.erb <h1>Editing Book</h1> <%= render 'form', book: @book %> <%= link_to 'Show', @book %> | <%= link_to 'Back', books_path %>
再びnew.html.erbのrender 'form', book: @book
に話を戻すと...
「book: @book」というオプションで部分テンプレートに渡す引数を指示している。具体的には、部分テンプレートが持つローカル変数bookにインスタンス変数@bookを対応付けている。
_form.html.erbの中のform_with(model: book, local: true) do |form|
にあるbookというローカル変数が、renderで指定される引数のbookに対応する。したがって、遡るとbookにはBooks.newでインスタンス化されたBookモデルのオブジェクトが渡されることになる。
createアクション
図書の新規登録を行うにはブラウザに表示された画面からタイトルと説明を入力し、「Create Book」ボタンを押すことで次のアクションのリクエストをRailsサーバに対して行うことになる。
ボタンを押すことによってhttp://localhost:3000/booksに対してHTTPリクエストが行われる。URIは図書一覧と同じだが、HTTPメソッドがPOSTである点が異なる。そのため、ルーターは7つのアクションのうちbooks#createを呼び出すことになる。
# app/controllers/books_controller.rb のcreateアクション部分 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 } format.json { render json: @book.errors, status: :unprocessable_entity } end end end
@book = Book.new(book_params)
はBookモデルをインスタンス化している。newアクションのときとは異なり、引数book_paramsを使用して入力された情報を取り込みインスタンス化する。
book_paramsはBooksコントローラーの中でprivateメソッドとして次のように実装されている。
# app/controllers/books_controller.rb のbook_paramsメソッド部分 private ...(省略) def book_params params.require(:book).permit(:title, :description) end
最初のparamsは受信パラメータのオブジェクトである。パラメータとして受け取った入力情報はparamsオブジェクトの中に保存されていると考えるといい。
このパラメータの中にあるBookモデル関連のパラメータのみを抜き出し、それをデータベースに保存できるように許可を与える作業(ストロングパラメータ化)を行っている。ストロングパラメータについては別途まとめようと思う。
books_controller.rbのcreateアクションに話を戻す。
respond_to do |format| ~ end
という箇所は、フォーマット(HTMLなどの形式)に基づいてブロック処理を行っている。こちらも詳細は別途まとめる予定。
保存が正しく行われていれば(if @book.save
)、redirect_to @book
を実行することになる。redirect_toはリダイレクト処理を行うことを意味し、Railsはredirect_toで指定されている@book(変数が保持する特定のインスタンス)によって、次の宛先(インスタンス内容を表示するルート)を自動で判断して設定する。具体的には、@bookインスタンスの新規で登録された図書IDに基づいて/books/:idのルート形式に相当するルートの宛先を作り出す。
リダイレクト
さらに、リダイレクトの後ろに指示されているオプションnotice: 'Book was successfully created.'
は、リダイレクトの宛先に渡すメッセージであり、noticeという変数にセットすることでリダイレクトされたアクションによって出力されるビューテンプレートに渡される。
そうして、リダイレクト先である/books/4(IDが4の場合)というリクエストに基づいて「GET /books/:id」に対応する「books#show」が呼ばれることになる。
ここで疑問が湧く。なんで@bookだけでshowって書いてないのにshowにとぶの...??
調べたら、ルーティング情報のRrefixがbookになっているところをみればわかった!!
ルーティング情報について↓
話を戻して、showアクションについて。
# app/controllers/books_controller.rb のshowアクション部分 def show end
before_action
上のshowアクションは何もしていないように見えるが、Booksコントローラーの最初の行に次のような記述がある。
# app/controllers/books_controller.rb の先頭部分 class BooksController < ApplicationController before_action :set_book, only: [:show, :edit, :update, :destroy]
before_actionはアクションの実行前に実行するメソッドを指示する。onlyで指定されるオプション[:show, :edit, :update, :destroy]
によってshowアクションが有効になっている。つまり、showアクションは先にset_bookメソッドを実行してから実行されることになる。
set_bookメソッドはprivateメソッドとして定義されている。
# app/controllers/books_controller.rb のset_bookメソッド部分 private def set_book @book = Book.find(params[:id]) end
Book.find(params[:id])
という記述は、
- Bookモデルを利用して
- リダイレクトの結果受信したパラメータのハッシュであるparamsオブジェクトを使用し
- params[:id]で参照するidの値(この場合は4)に相当する図書をデータベースの該当テーブルから取得する
ことを意味している。つまり@book = Book.find(params[:id])
は、受け取ったidの値に相当する図書のインスタンスを@bookに関係付けていることを意味する。
このbefore_actionのあとshowアクションが実行されるが、記述がないため同じ名前のshow.html.erbを呼び出す。
# app/views/books/show.html.erb <p id="notice"><%= notice %></p> <p> <strong>Title:</strong> <%= @book.title %> </p> <p> <strong>Description:</strong> <%= @book.description %> </p> <%= link_to 'Edit', edit_book_path(@book) %> | <%= link_to 'Back', books_path %>
最初の行の<%= notice %>で、変数noticeに渡された成功メッセージがブラウザに表示される。