本ガイドでは、Action ControllerとAction Viewによる基本的なレイアウト機能について解説します。
このガイドの内容:
本ガイドでは、コントローラ、ビュー、モデルによって形成される三角形のうち、コントローラとビューの間でのやりとりを中心に扱います。ご存じのように、Railsのコントローラはリクエストを扱うプロセス全体の流れを組織的に調整する責任を負い、(ビジネスロジックのような) 重い処理はモデルの方で行なうのが普通です。モデル側での処理が完了し、ユーザーに結果を表示する時がきたら、コントローラは処理結果をビューに渡します。このときの、コントローラからビューへの結果の渡し方こそが本ガイドの主なトピックです。
大きな流れとしては、ユーザーへのレスポンスとして送信すべき内容を決定することと、ユーザーへのレスポンスを作成するために適切なメソッドを呼び出すこともこの作業に含まれます。ユーザーに返すレスポンス画面を完全なビューにするのであれば、Railsはそのビューをさらに別のレイアウトでラッピングし、パーシャルビューとして取り出すでしょう。以後本ガイドではこれらの方法をすべて紹介します(訳注: 本ガイドではrenderを一般的な意味では「出力」、具体的な動作を指す場合は「レンダリング」と訳しています)。
コントローラ側から見ると、HTTPレスポンスの作成方法は以下の3とおりあります。
renderを呼び出し、ブラウザに返す完全なレスポンスを作成するredirect_toを呼び出し、HTTPリダイレクトコードステータスをブラウザに送信するheadを呼び出し、HTTPヘッダーのみで構成されたレスポンスを作成してブラウザに送信するRailsでは「設定より規約 (CoC: convention over configuration)」というポリシーが推奨されていることをご存じかと思います。デフォルトの出力結果は、CoCのよい例でもあります。Railsのコントローラは、デフォルトでは正しいルーティングに対応する名前を持つビューを自動的に選び、それを使用してレスポンスを出力します。たとえば、BooksControllerというコントローラに以下のコードがあるとします。
class BooksController < ApplicationController end
ルーティングファイルに以下が記載されているとします。
resources :books
app/views/books/index.html.erbビューファイルの内容が以下のようになっているとします。
<h1>Books are coming soon!</h1>
以上のようにすることで、ユーザーがブラウザで/booksにアクセスすると、Railsは自動的にapp/views/books/index.html.erbビューを使用してレスポンスを出力し、その結果「Books are coming soon!」という文字が画面に表示されます。
しかしこの画面だけではほとんど実用性がないので、Bookモデルを作成し、BooksControllerにindexアクションを追加してみましょう。
class BooksController < ApplicationController def index @books = Book.all end end
上のコードでご注目いただきたいのは、「設定より規約」の原則が利いているおかげでindexアクションの最後で明示的に画面出力を指示する必要がないという点です。ここでの原則は、「コントローラのアクションの最終部分で明示的な画面出力が指示されていない場合は、コントローラが使用できるビューのパスからアクション名.html.erbというビューテンプレートを探し、それを使用して自動的に出力する」というものです。従って、この場合はapp/views/books/index.html.erbファイルが出力されます。
ビューですべての本の属性を表示したい場合は、以下のようにERBを書くことができます。
<h1>Listing Books</h1> <table> <thead> <tr> <th>Title</th> <th>Content</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @books.each do |book| %> <tr> <td><%= book.title %></td> <td><%= book.content %></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 %>
実際のレンダリングは、ActionView::TemplateHandlersのサブクラスで行われます。本ガイドではレンダリングの詳細については触れませんが、テンプレートハンドラの選択がビューテンプレートファイルの拡張子によって制御されているという重要な点は理解しておいてください。Rails 2以降におけるビューテンプレートの標準拡張子は、ERB (HTML + eMbedded RuBy) でレンダリングする場合は.erb、Builder (XMLジェネレータ) でレンダリングする場合は.builderです。
renderを使用するアプリケーションがブラウザで表示するコンテンツのレンダリング (出力) という力仕事は、ActionController::Base#renderメソッドがほぼ一手に引き受けています。renderメソッドはさまざまな方法でカスタマイズできます。Railsテンプレートのデフォルトビューを出力することもできますし、特定のテンプレート、ファイル、インラインコードを指定して出力したり、何も出力しないこともできます。テキスト、JSON、XMLを出力することもできます。出力されるレスポンスのcontent typeやHTTPステータスを指定することもできます。
出力結果をブラウザで表示して調べることなく、render呼び出しの正確な結果を取得したい場合は、render_to_stringを呼び出すことができます。このメソッドの動作はrenderと完全に同じであり、出力結果をブラウザに返さずに文字列を返す点だけが異なります。
同じコントローラで、デフォルトと異なるテンプレートに対応するビューを出力したい場合は、renderメソッドでビュー名を指定することができます。
def update @book = Book.find(params[:id]) if @book.update(book_params) redirect_to(@book) else render "edit" end end
上のupdateアクションでモデルに対するupdateメソッドの呼び出しが失敗すると、同じコントローラに用意しておいた別のedit.html.erbテンプレートを使用して出力します。
出力するアクションを指定するには、文字列の他にシンボルを使用することもできます。
def update @book = Book.find(params[:id]) if @book.update(book_params) redirect_to(@book) else render :edit end end
あるコントローラのアクションから、まったく別のコントローラの配下にあるテンプレートを使用して出力することは可能でしょうか。これもrenderメソッドだけで行なうことができます。renderメソッドにはapp/viewsを起点とするフルパスを渡すことができますので、出力したいテンプレートをフルパスで指定します。たとえば、app/controllers/adminに置かれているAdminProductsコントローラのコードを実行しているとすると、app/views/productsに置かれているビューテンプレートに対するアクションの実行結果を出力するには以下のようにします。
render "products/show"
パスにスラッシュ/が含まれていると、Railsによってこのビューは異なるコントローラの配下にあると認識されます。異なるコントローラのテンプレートを指定していることをより明示的にしたい場合は、以下のように:templateオプションを使用することもできます (Rails 2.2以前ではこのオプションは必須でした)。
render template: "products/show"
renderメソッドで指定するビューは、現在のアプリケーションディレクトリの外部にあっても構いません (2つのRailsアプリケーションでビューを共有しているなどの場合)。
render "/u/apps/warehouse_app/current/app/views/products/show"
パスがスラッシュ/で始まっている場合、Railsはこのコードがファイルの出力であると認識します。ファイルを出力することをより明示的にしたい場合は、以下のように:fileオプションを使用することもできます (Rails 2.2以前ではこのオプションは必須でした)。
render file: "/u/apps/warehouse_app/current/app/views/products/show"
:fileオプションに与えるパスは、ファイルシステムの絶対パスです。当然ながら、コンテンツを出力したいファイルに対して適切なアクセス権が与えられている必要があります。
:fileオプションをユーザーの入力と組み合わせて使用すると、攻撃者がこのアクションを使用してファイルシステム内のセキュリティ上重要なファイルにアクセスする可能性があるため、セキュリティ上の問題が生じる可能性があります。
ファイルを出力する場合、デフォルトでは現在のレイアウトを使用してレンダリングされます。
Microsoft Windows上でRailsを実行している場合、ファイルを出力する際に:fileオプションを省略できません。Windowsのファイル名フォーマットはUnixのファイル名と同じではないためです。
これまでご紹介した3通りの出力方法 (コントローラ内の別テンプレートを使用、別のコントローラのテンプレートを使用、ファイルシステム上の任意のファイルを使用) は、実際には同一のアクションのバリエーションにすぎません。
実のところ、たとえばBooksControllerクラスのupdateアクション内で、本の更新に失敗したらeditテンプレートを出力したいとすると、以下のどのレンダリング呼び出しを行っても最終的には必ずviews/booksディレクトリのedit.html.erbを使用して出力が行われます。
render :edit render action: :edit render "edit" render "edit.html.erb" render action: "edit" render action: "edit.html.erb" render "books/edit" render "books/edit.html.erb" render template: "books/edit" render template: "books/edit.html.erb" render "/path/to/rails/app/views/books/edit" render "/path/to/rails/app/views/books/edit.html.erb" render file: "/path/to/rails/app/views/books/edit" render file: "/path/to/rails/app/views/books/edit.html.erb"
どの呼び出しを使用するかはコーディングのスタイルと規則の問題でしかありませんが、経験上なるべくシンプルな記法を使用する方がコードがわかりやすくなるでしょう。
renderで:inlineオプションを使用するrenderメソッドは、メソッド呼び出しの際に:inlineオプションを使用してERBを与えると、ビューがまったくない状態でも実行することができます。これは完全に有効な方法です。
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
このオプションを実際に使用する意味はほぼないと思われます。コントローラのコードにERBを混在させると、RailsのMVC指向が崩されるだけでなく、開発者がプロジェクトのロジックを追いかけることが困難になってしまいます。通常のERBビューを使用してください。
インラインでは、デフォルトでERBを使用して出力を行います。:typeオプションで:builderを指定すると、ERBに代えてBuilderが使用されます。
render inline: "xml.p {'Horrid coding practice!'}", type: :builder
renderで:plainオプションを使用すると、平文テキストをマークアップせずにブラウザに送信することができます。
render plain: "OK"
平文テキストの出力は、AjaxやWebサービスリクエストに応答するときに最も有用です。これらではHTML以外の応答を期待しています。
デフォルトでは、:plainオプションを使用すると出力結果に現在のレイアウトが適用されません。テキストの出力を現在のレイアウト内で行いたい場合は、layout: trueオプションを追加して.text.erbを使う必要があります。
renderで:htmlオプションを使用すると、HTML文字列を直接ブラウザに送信することができます。
render html: helpers.tag.strong('Not Found')
この手法は、HTMLコードのごく小規模なスニペットを出力したい場合に便利です。 スニペットのマークアップが複雑になるようであれば、早めにテンプレートファイルに移行することをご検討ください。
html:オプションを使用すると、html_safeを理解できるAPIで文字列が組み立てられていない場合にHTMLエンティティがエスケープされます。
JSONはJavaScriptのデータ形式の一種で、多くのAjaxライブラリで使用されています。Railsでは、オブジェクトからJSON形式への変換と、変換されたJSONをブラウザに送信する機能がビルトインでサポートされています。
render json: @product
出力するオブジェクトに対してto_jsonを呼び出す必要はありません。:jsonオプションが指定されていれば、renderによってto_jsonが自動的に呼び出されるようになっています。
Railsでは、オブジェクトからXML形式への変換と、変換されたXMLをブラウザに送信する機能がビルトインでサポートされています。
render xml: @product
出力するオブジェクトに対してto_xmlを呼び出す必要はありません。:xmlオプションが指定されていれば、renderによってto_xmlが自動的に呼び出されるようになっています。
Railsはvanilla JavaScriptを出力することもできます。
render js: "alert('Hello Rails');"
上のコードは、引数で与えられた文字列をMIMEタイプtext/javascriptでブラウザに送信します。
renderで:bodyオプションを指定することで、content typeを一切指定しない生のコンテンツをブラウザに送信することができます。
render body: "raw"
このオプションを使用するのは、レスポンスのcontent typeがどんなものであってもよい場合のみにしてください。ほとんどの場合、:plainや:htmlなどを使用する方が適切です。
このオプションを使用してブラウザに送信されるレスポンスは、上書きされない限りtext/plainが使用されます。これはAction Dispatchによるレスポンスのデフォルトのcontent typeであるためです。
renderのオプションrenderメソッドに対する呼び出しでは、一般に以下の5つのオプションが使用できます。
:content_type:layout:location:status:formats:content_typeオプションRailsがデフォルトで出力する結果のMIME content-typeは、デフォルトでtext/htmlになります (ただし:jsonを指定した場合にはapplication/json、:xmlを使用した場合はapplication/xmlになります)。content-typeを変更したい場合は、:content_typeオプションを指定します。
render file: filename, content_type: "application/rss"
:layoutオプションrenderで指定できるほとんどのオプションでは、出力されるコンテンツは現在のレイアウトの一部としてブラウザ上で表示されます。これより、レイアウトの詳細と利用法について本ガイドで説明します。
:layoutオプションを指定すると、現在のアクションに対して特定のファイルをレイアウトとして使用します。
render layout: "special_layout"
出力時にレイアウトをまったく使用しないよう指定することもできます。
render layout: false
:locationオプション:locationを使用することで、HTTPのLocationヘッダーを設定できます。
render xml: photo, location: photo_url(photo)
:statusオプションRailsが返すレスポンスのHTTPステータスコードは自動的に生成されます (ほとんどの場合200 OKとなります)。:statusオプションを使用することで、レスポンスのステータスコードを変更できます。
render status: 500 render status: :forbidden
ステータスコードは数字で指定する他に、以下に示すシンボルで指定することもできます。
| レスポンスクラス | HTTPステータスコード | シンボル |
|---|---|---|
| Informational | 100 | :continue |
| 101 | :switching_protocols | |
| 102 | :processing | |
| Success | 200 | :ok |
| 201 | :created | |
| 202 | :accepted | |
| 203 | :non_authoritative_information | |
| 204 | :no_content | |
| 205 | :reset_content | |
| 206 | :partial_content | |
| 207 | :multi_status | |
| 208 | :already_reported | |
| 226 | :im_used | |
| Redirection | 300 | :multiple_choices |
| 301 | :moved_permanently | |
| 302 | :found | |
| 303 | :see_other | |
| 304 | :not_modified | |
| 305 | :use_proxy | |
| 307 | :temporary_redirect | |
| 308 | :permanent_redirect | |
| Client Error | 400 | :bad_request |
| 401 | :unauthorized | |
| 402 | :payment_required | |
| 403 | :forbidden | |
| 404 | :not_found | |
| 405 | :method_not_allowed | |
| 406 | :not_acceptable | |
| 407 | :proxy_authentication_required | |
| 408 | :request_timeout | |
| 409 | :conflict | |
| 410 | :gone | |
| 411 | :length_required | |
| 412 | :precondition_failed | |
| 413 | :payload_too_large | |
| 414 | :uri_too_long | |
| 415 | :unsupported_media_type | |
| 416 | :range_not_satisfiable | |
| 417 | :expectation_failed | |
| 421 | :misdirected_request | |
| 422 | :unprocessable_entity | |
| 423 | :locked | |
| 424 | :failed_dependency | |
| 426 | :upgrade_required | |
| 428 | :precondition_required | |
| 429 | :too_many_requests | |
| 431 | :request_header_fields_too_large | |
| 451 | :unavailable_for_legal_reasons | |
| Server Error | 500 | :internal_server_error |
| 501 | :not_implemented | |
| 502 | :bad_gateway | |
| 503 | :service_unavailable | |
| 504 | :gateway_timeout | |
| 505 | :http_version_not_supported | |
| 506 | :variant_also_negotiates | |
| 507 | :insufficient_storage | |
| 508 | :loop_detected | |
| 510 | :not_extended | |
| 511 | :network_authentication_required |
「non-content」ステータスコード (100-199、204、205、 304のいずれか)でレンダリングしようとすると、レスポンスから削除されます。
:formatsオプションRailsは、リクエストで指定されたフォーマットを使います(またはデフォルトのhtml)。:formatsに渡すオプションは、シンボルや配列で変更できます。
render formats: :xml render formats: [:json, :xml]
指定されたフォーマットのテンプレートが存在しない場合は、ActionView::MissingTemplateエラーになります。
Railsは現在のレイアウトを探索する場合、最初に現在のコントローラと同じ基本名を持つレイアウトがapp/views/layoutsディレクトリにあるかどうかを調べます。たとえば、PhotosControllerクラスのアクションから出力するのであれば、app/views/layouts/photos.html.erbまたはapp/views/layouts/photos.builderを探します。該当のコントローラに属するレイアウトがない場合、app/views/layouts/application.html.erbまたはapp/views/layouts/application.builderを使用します。.erbレイアウトがない場合、.builderレイアウトがあればそれを使用します。Railsには、各コントローラやアクションに割り当てる特定のレイアウトをもっと正確に指定する方法がいくつも用意されています。
layout宣言を使用することで、デフォルトのレイアウト名ルールを上書きすることができます。例:
class ProductsController < ApplicationController layout "inventory" #... end
この宣言によって、ProductsControllerからの出力で使用されるレイアウトはapp/views/layouts/inventory.html.erbになります。
アプリケーション全体で特定のレイアウトを使用したい場合は、ApplicationControllerクラスでlayoutを宣言します。
class ApplicationController < ActionController::Base layout "main" #... end
この宣言によって、アプリケーションのすべてのビューで使用されるレイアウトはapp/views/layouts/main.html.erbになります。
レイアウトの指定にシンボルを使用することで、リクエストが実際に処理されるときまでレイアウトを確定せず、選択を遅延することができます。
class ProductsController < ApplicationController layout :products_layout def show @product = Product.find(params[:id]) end private def products_layout @current_user.special? ? "special" : "products" end end
上のコードは、現在のユーザーが特別なユーザーの場合、そのユーザーが製品ページを見るときに特別なレイアウトを適用します。
レイアウトを決定する際に、Procなどのインラインメソッドを使用することもできます。たとえばProcオブジェクトを渡すと、Procを渡されたブロックにはcontrollerインスタンスが渡されます。これにより、現在のリクエストを元にしてレイアウトを決定することができます。
class ProductsController < ApplicationController layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" } end
コントローラレベルで指定されたレイアウトでは、:onlyオプションと:exceptオプションがサポートされています。これらのオプションは、単一のメソッド名またはメソッド名の配列を引数として受け取ります。渡すメソッド名はコントローラ内のメソッド名に対応します。
class ProductsController < ApplicationController layout "product", except: [:index, :rss] end
上の宣言によって、rssメソッドとindexメソッド以外のすべてのメソッドにproductレイアウトが適用されます。
レイアウト宣言は下の階層に継承されます。下の階層、つまりより具体的なレイアウト宣言は、上の階層、つまりより一般的なレイアウトよりも常に優先されます。例:
application_controller.rb
class ApplicationController < ActionController::Base layout "main" end
posts_controller.rb
class PostsController < ApplicationController end
special_posts_controller.rb
class SpecialPostsController < PostsController layout "special" end
old_posts_controller.rb
class OldPostsController < SpecialPostsController layout false def show @post = Post.find(params[:id]) end def index @old_posts = Post.older render layout: "old" end # ... end
上のアプリケーションは以下のように動作します。
mainレイアウトが使用されます。PostsController#indexではmainレイアウトが使用されます。SpecialPostsController#indexではspecialレイアウトが使用されます。OldPostsController#showではレイアウトが適用されません。OldPostsController#indexではoldレイアウトが使用されます。レイアウト継承のロジックと同様に、テンプレートやパーシャルが通常のパスで見つからない場合、コントローラーは継承パスを探索してレンダリングするテンプレートやパーシャルを見つけようとします。次の例をご覧ください。
# app/controllers/application_controller class ApplicationController < ActionController::Base end # app/controllers/admin_controller class AdminController < ApplicationController end # app/controllers/admin/products_controller class Admin::ProductsController < AdminController def index end end
このときのadmin/products#indexアクションの探索順序は次のようになります。
app/views/admin/products/app/views/admin/app/views/application/つまり、app/views/application/は共有パーシャルの置き場所として手頃です。これらはERBで次のようにレンダリングされます。
<%# app/views/admin/products/index.html.erb %> <%= render @products || "empty_list" %> <%# app/views/application/_empty_list.html.erb %> There are no items in this list <em>yet</em>.
Rails開発をやっていれば、一度は "Can only render or redirect once per action" エラーに遭遇したことがあるでしょう。いまいましいエラーですが、修正は比較的簡単です。このエラーはほとんどの場合、開発者がrenderメソッドの基本的な動作を誤って理解していることが原因です。
このエラーを発生する以下のコードを例にとって説明しましょう。
def show @book = Book.find(params[:id]) if @book.special? render action: "special_show" end render action: "regular_show" end
@book.special?がtrueの場合、Railsはレンダリングを開始し、@book変数をspecial_showビューに転送します。しかし、showアクションのコードはそこで 止まらない ことにご注意ください。showアクションのコードは最終行まで実行され、regular_showビューのレンダリングを行おうとした時点でエラーが発生します。解決法はいたって単純です。1つのコード実行パス内では、renderメソッドやredirectメソッドの実行は1度だけにしてください。ここで非常に便利なのがand returnというメソッドです。このメソッドを使用して修正したバージョンを以下に示します。
def show @book = Book.find(params[:id]) if @book.special? render action: "special_show" and return end render action: "regular_show" end
&& returnではなくand returnを使用してください。&& returnはRuby言語の&&演算子の優先順位が高すぎてこの文脈では正常に動作しません。
RailsにビルトインされているActionControllerが行なう暗黙のレンダリングでは、renderメソッドが呼び出されたかどうかを確認してからレンダリングを開始します。従って、以下のコードは正常に動作します。
def show @book = Book.find(params[:id]) if @book.special? render action: "special_show" end end
上のコードは、ある本がspecial?である場合にのみspecial_showテンプレートを使用して出力します。それ以外の場合はshowテンプレートを使用して出力します。
redirect_toを使用するHTTPリクエストにレスポンスを返すもう一つの方法は、redirect_toを使用することです。前述のとおり、renderはレスポンス構成時にどのビュー (または他のアセット) を使用するかを指定するためのものです。redirect_toメソッドは、この点においてrenderメソッドと根本的に異なります。redirect_toメソッドは、別のURLに対して改めてリクエストを再送信するよう、ブラウザに指令を出すためのものです。たとえば以下の呼び出しを行なうと、アプリケーションで現在どのページが表示されていても、写真のインデックス表示ページにリダイレクトされます。
redirect_to photos_url
redirect_backを使うと、ユーザを直前のページに戻すことができます。戻る場所はHTTP_REFERERヘッダを利用していますが、これはブラウザが必ず設定しているとは限りません。そのため、fallback_locationは必ず設定しなければなりません。
redirect_backを使うと、ユーザを直前のページに戻すことができます。戻り先の場所はHTTP_REFERERヘッダーから取り出されますが、これがブラウザ側で設定される保証はありません。したがって、ここではfallback_locationを指定する必要があります。
redirect_back(fallback_location: root_path)
redirect_toやredirect_backはメソッドの実行を即座に中断したりはしません。これらは単にHTTPのレスポンスを設定するだけです。もしこれらの後にメソッドがあった場合そのメソッドは実行されてしまいます。必要であれば、明示的なreturnもしくは他の中断用の手法を使うことで中断可能です。
redirect_toを呼び出すと、一時的なリダイレクトを意味するHTTPステータスコード302がブラウザに返され、ブラウザはそれに基いてリダイレクトを行います。別のステータスコード (301: 恒久的なリダイレクト) に変更するには:statusオプションを使用します。
redirect_to photos_path, status: 301
renderの:statusオプションの場合と同様、redirect_toの:statusもヘッダーを指定する時に数値の他にシンボルも使用できます。
renderとredirect_toの違いときおり、redirect_toを一種のgotoコマンドとして理解している開発初心者を見かけます。Railsコードの実行位置をある場所から別の場所に移動するコマンドであると考えているわけです。これは 正しくありません 。redirect_toを実行した後、コードはそこで実行を終了し、ブラウザからの次のリクエストを待ちます (通常のスタンバイ状態)。その直後、redirect_toでブラウザに送信したHTTPステータスコード302に従って、ブラウザから別のURLへのリクエストがサーバーに送信され、サーバーはそのリクエストを改めて処理します。それ以外のことは行っていません。
renderとredirect_toの違いを以下のアクションで比較してみましょう。
def index @books = Book.all end def show @book = Book.find_by(id: params[:id]) if @book.nil? render action: "index" end end
上のフォームのコードでは、@bookインスタンス変数がnilの場合に問題が生じる可能性があります。render :actionは、対象となるアクションのコードを実行しないことを覚えておいてください。このため、indexビューでおそらく必要となる@booksインスタンス変数には何も設定されず、空の蔵書リストが表示されてしまいます。これを修正する方法のひとつは、renderをredirectに変更することです。
def index @books = Book.all end def show @book = Book.find_by(id: params[:id]) if @book.nil? redirect_to action: :index end end
上のコードであれば、ブラウザから改めてindexページにリクエストが送信されるので、indexメソッドのコードが正常に実行されます。
上のコードで1つ残念な点があるとすれば、ブラウザとのやりとりが1往復増えることです。ブラウザから/books/1に対してshowアクションが呼び出され、コントローラが本が1冊もないことを検出すると、コントローラはブラウザに対してステータスコード302 (リダイレクト) レスポンスを返し、/books/に再度アクセスするようブラウザに指令を出します。ブラウザはこの指令に応じ、このコントローラのindexアクションを呼び出すためのリクエストを改めてサーバーに送信します。そしてコントローラはこのリクエストを受けてデータベースからすべての蔵書リストを取り出し、indexテンプレートをレンダリングして出力結果をブラウザに送り返すと、ブラウザで蔵書リストが表示されます。
このやりとりの増加による遅延は、小規模なアプリケーションであればおそらく問題になりませんが、遅延が甚だしくなってきた場合にはこの点を改める必要があるかもしれません。ブラウザとのやりとりを増やさないように工夫した例を以下に示します。
def index @books = Book.all end def show @book = Book.find_by(id: params[:id]) if @book.nil? @books = Book.all flash.now[:alert] = "Your book was not found" render "index" end end
上のコードの動作は次のとおりです。指定されたidを持つ本が見つからない場合は、モデル内のすべての蔵書リストを@booksインスタンス変数に保存します。続いてflashによる警告メッセージを追加し、さらにindex.html.erbテンプレートを直接レンダリングしてから出力結果をブラウザに送り返します。
headでヘッダのみのレスポンスを生成するheadメソッドを使用することで、ヘッダだけで本文 (body) のないレスポンスをブラウザに送信できます。headメソッドには、HTTPステータスコードを示す多くのシンボルを引数として指定できます (参照テーブル 参照)。オプションの引数はヘッダ名と値をペアにしたハッシュ値として解釈されます。たとえば、以下のコードはエラーヘッダーのみのレスポンスを返すことができます。
head :bad_request
上のコードによって以下のヘッダーが生成されます。
HTTP/1.1 400 Bad Request Connection: close Date: Sun, 24 Jan 2010 12:15:53 GMT Transfer-Encoding: chunked Content-Type: text/html; charset=utf-8 X-Runtime: 0.013483 Set-Cookie: _blog_session=...snip...; path=/; HttpOnly Cache-Control: no-cache
以下のように、ヘッダーに別の情報を含めることもできます。
head :created, location: photo_path(@photo)
上のコードの結果は以下のようになります。
HTTP/1.1 201 Created Connection: close Date: Sun, 24 Jan 2010 12:16:44 GMT Transfer-Encoding: chunked Location: /photos/1 Content-Type: text/html; charset=utf-8 X-Runtime: 0.083496 Set-Cookie: _blog_session=...snip...; path=/; HttpOnly Cache-Control: no-cache
Railsがビューからレスポンスを出力するときには、そのビューには現在のレイアウトも組み込まれます。現在のレイアウトを探索するときのルールは、本ガイドで既に説明したものが使用されます。レイアウト内では、さまざまな出力の断片を組み合わせて最終的なレスポンス出力を得るための3つのツールを利用できます。
yield and content_for
アセットタグヘルパーが提供するメソッドは、フィード、JavaScript、スタイルシート、画像、動画および音声のビューにリンクするHTMLを生成するためのものです。Railsでは以下の6つのアセットタグヘルパーが利用できます。
auto_discovery_link_tagjavascript_include_tagstylesheet_link_tagimage_tagvideo_tagaudio_tagこれらのタグは、レイアウトや別のビューで使用することもできます。このうち、auto_discovery_link_tag、javascript_include_tag、stylesheet_link_tagはレイアウトの<head>セクションで使用するのが普通です。
これらのアセットタグヘルパーは、指定の場所にアセットがあるかどうかを 検証しません 。
auto_discovery_link_tagを使用してフィードにリンクするauto_discovery_link_tagヘルパーを使用すると、多くのブラウザやフィードリーダーでRSSフィードやAtomフィード、JSONフィードを検出できるHTMLが生成されます。このメソッドが受け取れる引数は、リンクの種類 (:rssまたは:atom、:json)、url_forで渡されるオプションのハッシュ、およびタグのハッシュです。
<%= auto_discovery_link_tag(:rss, {action: "feed"}, {title: "RSS Feed"}) %>
auto_discovery_link_tagでは以下の3つのタグオプションが使用できます。
:relはリンク内のrel値を指定します。デフォルト値は "alternate" です。:typeはMIMEタイプを明示的に指定したい場合に使用します。通常、Railsは適切なMIMEタイプを自動的に生成します。:titleはリンクのタイトルを指定します。デフォルト値は:type値を大文字にしたものです ("ATOM" や "RSS" など)。javascript_include_tagを使用してJavaScriptファイルにリンクするjavascript_include_tagヘルパーは、指定されたソースごとにHTML scriptタグを返します。
Railsでアセットパイプライン を有効にしている場合、JavaScriptへのリンク先は旧Railsのpublic/javascriptsではなく/assets/javascripts/になります。その後このリンクはアセットパイプラインによって利用可能になります。
Railsアプリケーション内やRailsエンジン内のJavaScriptファイルは、app/assets、lib/assets、vendor/assetsのいずれかの場所に置かれます。これらの置き場所の詳細については、アセットパイプラインガイドの「アセットの編成」 を参照してください。
好みに応じて、ドキュメントルートからの相対フルパスやURLを指定することもできます。たとえば、app/assets、lib/assets、またはvendor/assetsの下にあるjavascriptsの下にあるJavaScriptファイルにリンクしたい場合は以下のようにします。
<%= javascript_include_tag "main" %>
上のコードにより、以下のようなscriptタグが出力されます。
<script src='/assets/main.js'></script>
このアセットへのリクエストは、Sprockets gemによって提供されます。
複数のファイルにアクセスしたい場合 (app/assets/javascripts/main.jsとapp/assets/javascripts/columns.jsなど) は以下のようにします。
<%= javascript_include_tag "main", "columns" %>
app/assets/javascripts/main.jsとapp/assets/javascripts/photos/columns.jsを含めたい場合は以下のようにします。
<%= javascript_include_tag "main", "/photos/columns" %>
http://example.com/main.jsを含めるには以下のようにします。
<%= javascript_include_tag "http://example.com/main.js" %>
stylesheet_link_tagを使用してCSSファイルにリンクするstylesheet_link_tagヘルパーは、提供されたソースごとにHTML <link>タグを返します。
Railsでアセットパイプラインを有効にしている場合、このヘルパーは/assets/stylesheets/へのリンクを生成します。その後このリンクはSprockets gemによって処理されます。スタイルシートファイルは、app/assets、lib/assets、またはvendor/assetsのいずれかの場所に置かれます。
ドキュメントルートからの相対フルパスやURLを指定することもできます。たとえば、app/assets、lib/assets、またはvendor/assetsの下にあるstylesheetsの下にあるスタイルシートファイルにリンクしたい場合は以下のようにします。
<%= stylesheet_link_tag "main" %>
app/assets/railsguides/stylesheets/main.cssとapp/assets/stylesheets/columns.cssを含めるには、以下のようにします。
<%= stylesheet_link_tag "main", "columns" %>
app/assets/railsguides/stylesheets/main.cssとapp/assets/stylesheets/photos/columns.cssを含めるには以下のようにします。
<%= stylesheet_link_tag "main", "photos/columns" %>
http://example.com/main.cssを含めるには以下のようにします。
<%= stylesheet_link_tag "http://example.com/main.css" %>
デフォルトでは、stylesheet_link_tagによって作成されるリンクにはmedia="screen" rel="stylesheet"という属性が含まれます。適切なオプション (:media, :rel) を使用することで、これらのデフォルト値を上書きできます。
<%= stylesheet_link_tag "main_print", media: "print" %>
image_tagを使用して画像にリンクするimage_tagは、特定のファイルを指すHTML <img />タグを生成します。デフォルトでは、ファイルはpublic/images以下から読み込まれます。
画像ファイルの拡張子は省略できません。
<%= image_tag "header.png" %>
好みに応じて、画像ファイルへのパスを直接指定することもできます。
<%= image_tag "icons/delete.gif" %>
ハッシュ形式で与えられたHTMLオプションを追加することもできます。
<%= image_tag "icons/delete.gif", {height: 45} %>
ユーザーがブラウザで画像を非表示にしている場合、alt属性のテキストを表示することができます。alt属性が明示的に指定されていない場合は、ファイル名がaltテキストとして使用されます。このときファイル名の先頭は大文字になり、拡張子は取り除かれます。たとえば、以下の2つのimage_tagヘルパーは同じコードを返します。
<%= image_tag "home.gif" %> <%= image_tag "home.gif", alt: "Home" %>
"{幅}x{高さ}"という形式で特殊なsizeタグを指定することもできます。
<%= image_tag "home.gif", size: "50x20" %>
上の特殊タグ以外にも、:classや:idや:nameなどの標準的なHTMLオプションを最終的にハッシュにしたものを引数として与えることができます。
<%= image_tag "home.gif", alt: "Go Home", id: "HomeImage", class: "nav_bar" %>
video_tagを使用してビデオにリンクするvideo_tagヘルパーは、指定されたファイルを指すHTML 5 <video>タグを生成します。デフォルトでは、ファイルはpublic/videosから読み込まれます。
<%= video_tag "movie.ogg" %>
上のコードによって以下が生成されます。
<video src="/videos/movie.ogg" />
image_tagの場合と同様、絶対パスまたはpublic/videosディレクトリからの相対パスを指定できます。さらに、image_tagの場合と同様に、size: "#{幅}x#{高さ}"オプションを指定することもできます。ビデオタグでは、idやclassなどのHTMLオプションを末尾で自由に指定することもできます。
ビデオタグでは、<video> HTMLオプションを以下のようなHTMLオプションハッシュ形式で指定することもできます。
poster: "image_name.png"は、ビデオ再生前にビデオの位置に表示しておきたい画像を指定します。autoplay: trueは、ページの読み込み時にビデオを再生します。loop: trueは、ビデオを最後まで再生し終わったらループします。controls: trueは、ブラウザが提供するビデオ制御機能を使用できるようにします。autobuffer: trueは、ページ読み込み時にすぐ再生できるようにビデオを事前に読み込んでおきます。video_tagにビデオファイルの配列を渡すことで、複数のビデオを再生することもできます。
<%= video_tag ["trailer.ogg", "movie.ogg"] %>
上のコードによって以下が生成されます。
<video> <source src="/videos/trailer.ogg"> <source src="/videos/movie.ogg"> </video>
audio_tagを使用して音声ファイルにリンクするaudio_tagは、指定されたファイルを指すHTML 5 <audio>タグを生成します。デフォルトでは、これらのファイルはpublic/audios以下から読み込まれます。
<%= audio_tag "music.mp3" %>
好みに応じて、音声ファイルへのパスを直接指定することもできます。
<%= audio_tag "music/first_song.mp3" %>
:idや:classなどのオプションをハッシュ形式で指定することもできます。
video_tagの場合と同様、audio_tagにも以下の特殊オプションがあります。
autoplay: trueはページ読み込み時に音声ファイルを再生します。controls: trueは、ブラウザが提供する音声ファイル制御機能を使用できるようにします。autobuffer: trueは、ページ読み込み時にすぐ再生できるように音声ファイルを事前に読み込んでおきます。yieldを理解するyieldメソッドは、レイアウトのコンテキストでビューを挿入すべき場所を指定するのに使用します。yieldの最も単純な使用法は、yieldを1つだけ使用して、現在レンダリングされているビューのコンテンツ全体をその場所に挿入するというものです。
<html> <head> </head> <body> <%= yield %> </body> </html>
yieldを行なう領域を複数使用するレイアウトを作成することもできます。
<html> <head> <%= yield :head %> </head> <body> <%= yield %> </body> </html>
ビューのメイン部分は常に「名前のない」yieldとしてレンダリングされます。コンテンツを名前付きのyieldとしてレンダリングするには、content_forメソッドを使用します。
content_forを使用するcontent_forメソッドを使用することで、コンテンツを名前付きのyieldブロックとしてレイアウトに挿入できます。たとえば、以下のビューのレンダリング結果は上で紹介したレイアウト内に挿入されます。
<% content_for :head do %> <title>A simple page</title> <% end %> <p>Hello, Rails!</p>
このページのレンダリング結果がレイアウトに挿入されると、最終的に以下のHTMLが出力されます。
<html> <head> <title>A simple page</title> </head> <body> <p>Hello, Rails!</p> </body> </html>
content_forメソッドは、たとえばレイアウトが「サイドバー」や「フッター」などの領域に分かれていて、それらに異なるコンテンツを挿入したいような場合に大変便利です。あるいは、多くのページで使用する共通のヘッダーがあり、このヘッダーに特定のページでのみJavaScriptやCSSファイルを挿入したい場合にも便利です。
部分テンプレートは通常単にパーシャルと呼ばれます。パーシャルは、上とは異なる方法でレンダリング処理を扱いやすい単位に分割するためのしくみです。パーシャルを使用すると、レスポンスで表示するページの特定部分をレンダリングするためのコードを別ファイルに保存しておくことができます。
パーシャルをビューの一部に含めて出力するには、ビュー内でrenderメソッドを使用します。
<%= render "menu" %>
レンダリング中のビュー内に置かれている上のコードは、その場所で_menu.html.erbという名前のファイルをレンダリングします。パーシャルファイル名の冒頭にはアンダースコアが付いていることにご注意ください。これは通常のビューと区別するために付けられています。ただしrenderで呼び出す際にはこのアンダースコアは不要です。以下のように、他のフォルダの下にあるパーシャルを呼び出す際にもアンダースコアは不要です。
<%= render "shared/menu" %>
上のコードは、app/views/shared/_menu.html.erbパーシャルの内容をその場所でレンダリングします。
パーシャルの使用方法の1つは、パーシャルを一種のサブルーチンのようにみなすことです。詳細な表示内容をパーシャル化してビューから追い出し、コードを読みやすくします。例として、以下のようなビューがあるとします。
<%= render "shared/ad_banner" %> <h1>Products</h1> <p>Here are a few of our fine products:</p> ... <%= render "shared/footer" %>
上のコードの_ad_banner.html.erbパーシャルと_footer.html.erbパーシャルに含まれるコンテンツは、アプリケーションの多くのページと共有できます。あるページを開発中、パーシャルの部分については詳細を気にせずに済みます。
本ガイドの直前のセクションで説明したように、yieldはレイアウトを簡潔に保つ上で極めて強力なツールです。これは純粋なRubyなのでほぼどこでも利用できることを頭に置いておきましょう。たとえばyieldを用いると、多くの類似リソース向けのフォームレイアウト定義をDRYに書けます。
users/index.html.erb
<%= render "shared/search_filters", search: @q do |f| %> <p> Name contains: <%= f.text_field :name_contains %> </p> <% end %>
roles/index.html.erb
<%= render "shared/search_filters", search: @q do |f| %> <p> Title contains: <%= f.text_field :title_contains %> </p> <% end %>
shared/_search_filters.html.erb
<%= form_for(search) do |f| %> <h1>Search form:</h1> <fieldset> <%= yield f %> </fieldset> <p> <%= f.submit "Search" %> </p> <% end %>
すべてのページで共有されているコンテンツであれば、パーシャルをレイアウトで使用することができます。
ビューにレイアウトがあるのと同様、パーシャルでも独自のレイアウトファイルを使用することができます。たとえば、以下のようなパーシャルを呼び出すとします。
<%= render partial: "link_area", layout: "graybar" %>
上のコードは、_link_area.html.erbという名前のパーシャルを探し、_graybar.html.erbという名前のレイアウトを使用してレンダリングを行います。パーシャルレイアウトは、対応する通常のパーシャルと同様、名前の先頭にアンダースコアを追加する必要があります。そして、パーシャルとそれに対応するパーシャルレイアウトは同じディレクトリに置く必要があります。パーシャルレイアウトはlayoutsフォルダーには置けませんのでご注意ください。
:layoutなどの追加オプションを渡す場合は、:partialオプションを明示的に指定する必要がある点にもご注意ください。
パーシャルにローカル変数を引数として渡し、パーシャルをさらに強力かつ柔軟にすることもできます。たとえば、newページとeditページの違いがごくわずかしかないのであれば、この手法を使用してコードの重複を解消することができます。
new.html.erb
<h1>New zone</h1> <%= render partial: "form", locals: {zone: @zone} %>
edit.html.erb
<h1>Editing zone</h1> <%= render partial: "form", locals: {zone: @zone} %>
_form.html.erb
<%= form_for(zone) do |f| %> <p> <b>Zone name</b><br> <%= f.text_field :name %> </p> <p> <%= f.submit %> </p> <% end %>
上の2つのビューでは同じパーシャルがレンダリングされますが、Action Viewのsubmitヘルパーはnewアクションの場合には"Create Zone"を返し、editアクションの場合は"Update Zone"を返します。
どのパーシャルにも、パーシャル名からアンダースコアを取り除いた名前を持つローカル変数が与えられます。:objectオプションを使用することで、このローカル変数にオブジェクトを渡すことができます。
ローカル変数を特定の状況に限ってパーシャルに渡すには、local_assignsを使います。
index.html.erb<%= render user.articles %>
show.html.erb<%= render article, full: true %>
_article.html.erb<h2><%= article.title %></h2> <% if local_assigns[:full] %> <%= simple_format article.body %> <% else %> <%= truncate article.body %> <% end %>
このようにして、ローカル変数をすべて宣言する必要なしにパーシャルを使えるようになります。
どのパーシャルにも、パーシャル名と同じ名前(冒頭のアンダースコアの有無だけ異なる)のローカル変数が1つずつあります。objectオプションを使うと、このローカル変数にオブジェクトを1つ渡せます。
<%= render partial: "customer", object: @new_customer %>
上のcustomerパーシャル呼び出しでは、customerローカル変数は親のビューの@new_customer変数を指します。
あるモデルのインスタンスをパーシャルとしてレンダリングするのであれば、以下のような略記法を使用できます。
<%= render @customer %>
上のコードでは、@customerインスタンス変数にCustomerモデルのインスタンスが含まれているとします。この場合レンダリングには_customer.html.erbパーシャルが使用され、このパーシャルにはcustomerローカル変数が渡されます。このcustomerローカル変数は、親ビューにある@customerインスタンス変数を指します。
パーシャルはデータの繰り返し (コレクション) を出力する場合にもきわめて便利です。:collectionオプションを使用してパーシャルにコレクションを渡すと、コレクションのメンバごとにパーシャルがレンダリングされて挿入されます。
index.html.erb
<h1>Products</h1> <%= render partial: "product", collection: @products %>
_product.html.erb
<p>Product Name: <%= product.name %></p>
パーシャルを呼び出す時に指定するコレクションが複数形の場合、パーシャルの個別のインスタンスから、出力するコレクションの個別のメンバにアクセスが行われます。このとき、パーシャル名に基づいた名前を持つ変数が使用されます。上の場合、パーシャルの名前は_productであり、この_productパーシャル内でproductという名前の変数を使用して、出力されるインスタンスを取得できます。
このメソッドには略記法もあります。@productsがproductインスタンスのコレクションであるとすると、index.html.erbに以下のように書くことで同じ結果を得られます。
<h1>Products</h1> <%= render @products %>
使用するパーシャル名は、コレクション内のモデル名に基いて決定されます。実は、メンバが一様でない (さまざまな種類のメンバが入り混じった) コレクションにも上の方法を使用できます。この場合、コレクションのメンバに応じて適切なパーシャルが自動的に選択されます。
index.html.erb
<h1>Contacts</h1> <%= render [customer1, employee1, customer2, employee2] %>
customers/_customer.html.erb
<p>Customer: <%= customer.name %></p>
employees/_employee.html.erb
<p>Employee: <%= employee.name %></p>
上のコードでは、コレクションのメンバに応じて、customerパーシャルまたはemployeeパーシャルが自動的に選択されます。
コレクションが空の場合、renderはnilを返します。以下のような簡単な方法でもよいので、代わりのコンテンツを表示するようにしましょう。
<h1>Products</h1> <%= render(@products) || "There are no products available." %>
パーシャル内のローカル変数をカスタマイズしたい場合は、パーシャルの呼び出し時に:asオプションを指定します。
<%= render partial: "product", collection: @products, as: :item %>
上のように変更することで、@productsコレクションのインスタンスにitemという名前のローカル変数経由でアクセスできます。
locals: {}オプションを使用することで、レンダリング中のどのパーシャルにも任意の名前のローカル変数を渡すことができます。
<%= render partial: "product", collection: @products, as: :item, locals: {title: "Products Page"} %>
上の場合、titleという名前のローカル変数に"Products Page"という値が含まれており、パーシャルからこの値にアクセスできます。
コレクションによって呼び出されるパーシャル内でカウンタ変数を使用することもできます。このカウンタ変数は、パーシャルのタイトル名の後ろに_counterを追加した名前になります。たとえば、パーシャル内で@productsをレンダリングすると、_product.html.erbからproduct_counter変数を参照できます。product_counter変数は、それを囲むビュー内でレンダリングされた回数を示します。
:spacer_templateオプションを使用することで、メインパーシャルのインスタンスと交互にレンダリングされるセカンドパーシャルを指定することもできます。
<%= render partial: @products, spacer_template: "product_ruler" %>
上のコードでは、_productパーシャルと_productパーシャルの合間に_product_rulerパーシャル (引数なし) をレンダリングします。
コレクションをレンダリングするときにも:layoutオプションを指定できます。
<%= render partial: "product", collection: @products, layout: "special_layout" %>
このレイアウトは、コレクション内の各項目をレンダリングするたびに一緒にレンダリングされます。パーシャル内の場合と同様、このレイアウトでも現在のオブジェクトと(オブジェクト名)_counter変数を使用できます。
特定のコントローラをサポートするために、アプリケーションの標準レイアウトとの違いがごくわずかしかないようなレイアウトを使いたくなることがあります。ネストしたレイアウト (サブテンプレートと呼ばれることもあります) を使用することで、メインのレイアウトを複製して編集したりせずにこれを実現できます。例:
以下のApplicationControllerレイアウトがあるとします。
app/views/layouts/application.html.erb
<html> <head> <title><%= @page_title or "Page Title" %></title> <%= stylesheet_link_tag "layout" %> <style><%= yield :stylesheets %></style> </head> <body> <div id="top_menu">Top menu items here</div> <div id="menu">Menu items here</div> <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div> </body> </html>
NewsControllerによって生成されるページでは、トップメニューを隠して右メニューを追加したいとします。
app/views/layouts/news.html.erb
<% content_for :stylesheets do %> #top_menu {display: none} #right_menu {float: right; background-color: yellow; color: black} <% end %> <% content_for :content do %> <div id="right_menu">Right menu items here</div> <%= content_for?(:news_content) ? yield(:news_content) : yield %> <% end %> <%= render template: "layouts/application" %>
以上でおしまいです。Newsビューで新しいレイアウトが使用されるようになり、トップメニューが隠されて"content" divタグ内に右メニューが新しく追加されました。
これと同じ結果を得られるサブテンプレートの使用法はこの他にもさまざまなものが考えられます。ネスティングレベルには制限がない点にご注目ください。たとえばNewsレイアウトで新しいレイアウトを使用するために、render template: 'layouts/news'経由でActionView::renderメソッドを使用することもできます。Newsレイアウトをサブテンプレート化するつもりがないのであれば、content_for?(:news_content) ? yield(:news_content) : yieldを単にyieldに置き換えれば済みます。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。