本ガイドでは、Action ControllerとAction Viewによる基本的なレイアウト機能について解説します。
本ガイドの内容:
本ガイドでは、「コントローラ・ビュー・モデル」三角形のうち、コントローラとビューの間でのやりとりを中心に扱います。ご存じのように、Railsのコントローラはリクエスト処理プロセス全体の制御を担当し、(ビジネスロジックのような)重い処理はモデルの方で行なうのが普通です。モデルの処理が完了すると、コントローラは処理結果をビューに渡し、ビューはユーザーにレスポンスを返します。本ガイドでは、コントローラからビューに結果を渡す方法について解説します。
大きな流れとしては、まずユーザーに送信すべきレスポンスの内容を決定し、次にユーザーへのレスポンスを作成する適切なメソッドを呼び出します。レスポンス画面を完全なビューで作成すると、Railsはそのビューをレイアウトでラップして、場合によってはパーシャルビューもそこに追加します。本ガイドではこれらの方法をひととおり紹介します。
コントローラ側から見た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, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %></td> </tr> <% end %> </tbody> </table> <br> <%= link_to "New book", new_book_path %>
実際のレンダリングは、ActionView::Template::Handlersの名前空間の中でネストされたクラスで行われます。本ガイドではこれについて詳しく述べませんが、ここで重要なのは、ビューテンプレートファイルの拡張子によってテンプレートハンドラが自動的に選択されることを理解することです。
renderメソッドを使うほとんどの場合、アプリケーションがブラウザで表示するコンテンツのレンダリングには、コントローラの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, status: :unprocessable_entity end end
あるコントローラのアクションから、まったく別のコントローラの配下にあるテンプレートを利用してレンダリングすることは可能でしょうか。これもrenderメソッドでできます。
renderメソッドにはapp/viewsを起点とするフルパスを渡せるので、レンダリングするテンプレートをフルパスで指定します。たとえば、app/controllers/adminにあるAdminProductsコントローラのコードを実行する場合、以下のように書くことでアクションの実行結果をapp/views/productsに置かれているビューテンプレートでレンダリングできます。
render "products/show"
パスにスラッシュ/が含まれていると、Railsはこのビューが別のコントローラの配下にあることを認識します。別のコントローラのテンプレートを指定していることを明示的に指定したい場合は、以下のようにtemplate:オプションを使うこともできます (なおRails 2.2以前はtemplate:を省略できませんでした)。
render template: "products/show"
これまでご紹介した2とおりのレンダリング方法(コントローラ内の別テンプレートを使う、別のコントローラのテンプレートを使う)は、実際には同じアクションのバリエーションにすぎません。
たとえばBooksControllerクラスのupdateアクションで本の更新に失敗したらeditテンプレートをレンダリングしたいとします。しかし実際は、以下のどのレンダリング呼び出しを使っても、最終的なレンダリングではviews/booksディレクトリのedit.html.erbが使われます。
render :edit render action: :edit render "edit" render action: "edit" render "books/edit" render template: "books/edit"
どの呼び出しを使うかは開発チームのコーディングスタイルと規約の問題に過ぎませんが、経験則では、今書いているコードに合う最もシンプルな記法を使うのがよいでしょう。
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"
平文テキストのレンダリングは、HTML以外のレスポンスが期待されるAjaxやWebサービスリクエストにレスポンスを返すときに最も有用です。
デフォルトでは、: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が自動的に呼び出されます。
vanilla JavaScript(素の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 file: "#{Rails.root}/public/404.html", layout: false
上のコードは生のファイルをレンダリングします(ERBなどのハンドラはサポートされません)。デフォルトでは、現在のレイアウト内でレンダリングされます。
:fileオプションにユーザー入力を渡すと、セキュリティ上の問題につながる可能性があります(攻撃者がこのアクションを悪用してファイルシステム上の重要なファイルにアクセスする可能性があります)。
レイアウトが不要な場合は、多くの場合send_fileメソッドの方が高速かつ適切です。
Railsは、:render_inに応答できるオブジェクトを以下のようにレンダリングできます。
render MyRenderable.new
上のコードは、現在のビューコンテキストで指定のオブジェクト上のrender_inを呼び出します。
また、以下のようにrenderに:renderableオプションを指定すると、オブジェクトをレンダリングできます。
render renderable: MyRenderable.new
renderのオプション renderメソッド呼び出しでは、一般に以下の6つのオプションを指定できます。
:content_type:layout:location:status:formats:variants:content_typeオプションレンダリングのMIME Content-Typeヘッダーは、デフォルトでtext/htmlになります(ただし:jsonを指定するとapplication/json、:xmlを指定するとapplication/xmlになります)。Content-Typeを変更したい場合は、:content_typeオプションを指定します。
render template: "feed", 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オプション:formatsオプションは、リクエストで利用するフォーマットを指定します(デフォルトはhtml)。:formatsオプションにはシンボルまたは配列を渡せます。
render formats: :xml render formats: [:json, :xml]
指定されたフォーマットのテンプレートが存在しない場合は、ActionView::MissingTemplateエラーが発生します。
:variantsオプション:variantsオプションは、同じフォーマットの別テンプレートを探索するようRailsに指示します。
:variants`オプションに渡す別テンプレートのリストには、シンボルまたは配列が使えます。
以下は利用方法の例です。
# HomeController#indexで呼び出す render variants: [:mobile, :desktop]
上のコードで別テンプレートのセットを渡すと、以下のテンプレートを探索して、存在するテンプレートのうち最初のものが使われます。
app/views/home/index.html+mobile.erbapp/views/home/index.html+desktop.erbapp/views/home/index.html.erb指定のフォーマットを持つテンプレートが存在しない場合は、ActionView::MissingTemplateエラーが発生します。
render呼び出しで別テンプレートリストを指定する代わりに、以下のようにコントローラアクションのrequestオブジェクトで設定することもできます。
def index request.variant = determine_variant end private def determine_variant variant = nil # 別テンプレートを決定するコード variant = :mobile if session[:use_mobile] variant end end
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レイアウトが使われるようになります。
アプリケーション全体で特定のレイアウトを使いたい場合は、layoutをApplicationControllerクラスで宣言します。
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
articles_controller.rb
class ArticlesController < ApplicationController end
special_articles_controller.rb
class SpecialArticlesController < ArticlesController layout "special" end
old_articles_controller.rb
class OldArticlesController < SpecialArticlesController layout false def show @article = Article.find(params[:id]) end def index @old_articles = Article.older render layout: "old" end # ... end
上のアプリケーションは以下のように動作します。
mainレイアウトが使われる。ArticlesController#indexではmainレイアウトが使われる。SpecialArticlesController#indexではspecialレイアウトが使われる。OldArticlesController#showではレイアウトが適用されない。OldArticlesController#indexではoldレイアウトが使われる。レイアウト継承のロジックと同様に、テンプレートやパーシャルが通常のパスで見つからない場合、コントローラーは継承パスを探索してレンダリングするテンプレートやパーシャルを見つけようとします。以下の例で考えてみましょう。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base end
# app/controllers/admin_controller.rb class AdminController < ApplicationController end
# app/controllers/admin/products_controller.rb 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ビューのレンダリングを行おうとした時点でエラーが発生します。
解決法はいたって単純で、renderメソッドやredirectメソッドが1つのコード実行パス内で「1回だけ」呼び出されるようにすることです。こういうときには単にreturnと書くのがポイントです。以下はこの方法で修正したコードです。
def show @book = Book.find(params[:id]) if @book.special? render action: "special_show" return end render action: "regular_show" end
なお、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リクエストにレスポンスを返すもう1つの方法は、redirect_toを使う方法です。前述のとおり、renderはレスポンスを構成するときに使うビュー(または他のアセット)を指定しますが、redirect_toメソッドは、この点においてrenderメソッドと根本的に異なります。redirect_toメソッドは、別のURLにリクエストを再送信するようブラウザに指示します。たとえば以下の呼び出しを行なうと、アプリケーションで現在どのページが表示されていても、写真のindexページにリダイレクトされます。
redirect_to photos_url
redirect_backメソッドを使うと、直前のページに戻ります。戻る場所にはHTTP_REFERERヘッダから取り出した情報が使われますが、このヘッダーがブラウザ側で設定されているかどうかは保証されていないので、以下のように必ずfallback_locationを設定しなければなりません。
redirect_back(fallback_location: root_path)
redirect_toやredirect_backを呼び出しても、メソッドの実行がすぐに中断されるのではなく、単にHTTPのレスポンスが設定されます。もしこれらの呼び出しの後に別のメソッドがあると、そのメソッドは実行されてしまいます。必要であれば、明示的にreturnすることで(または別の方法で)中断できます。
redirect_toを呼び出すと、一時的なリダイレクトを意味するHTTPステータスコード302がブラウザに返され、ブラウザはそれに基いてリダイレクトを行います。別のステータスコード(おそらくHTTP 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インスタンス変数には何も設定されず、空の蔵書リストが表示されてしまいます。
これを修正する方法の1つは、renderを以下のようにredirect_toに変更することです。
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往復必要になることです。ブラウザから/books/1に対してshowアクションが呼び出され、本が1冊もないことをコントローラが検出すると、コントローラはブラウザにステータスコード302(リダイレクト)レスポンスを返し、/books/に再度アクセスするようブラウザに指示します。ブラウザはこの指示に沿って、コントローラのindexアクションを呼び出すリクエストを改めてサーバーに送信します。コントローラはこのリクエストを受け取って、データベースからすべての蔵書リストを取り出し、indexテンプレートをレンダリングして結果をブラウザに送り返すと、ブラウザで蔵書リストが表示されます。
このやりとりによる遅延は、小規模なアプリケーションであればおそらく問題になりませんが、場合によってはレスポンスの遅延が問題になることもあります。この問題を解決する方法の1つを以下のコードで説明します。
def index @books = Book.all end def show @book = Book.find_by(id: params[:id]) if @book.nil? @books = Book.all flash.now[:alert] = "この本は見つかりませんでした" 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とcontent_for
アセットタグヘルパーは、「フィード」「JavaScript」「スタイルシート」「画像」「動画」「音声」のビューにリンクするHTMLを生成するメソッドです。Railsでは以下の6つのアセットタグヘルパーが利用できます。
これらのタグは、レイアウトや別のビューでも利用できます。このうち、auto_discovery_link_tag、javascript_include_tag、stylesheet_link_tagはレイアウトの<head>セクションで使うのが普通です。
これらのアセットタグヘルパーは、指定の場所にアセットがあるかどうかを確認しません。単に指示どおりにリンクを生成します。
auto_discovery_link_tagでフィードへのリンクを生成するauto_discovery_link_tagヘルパーでHTMLを生成すると、さまざまなブラウザやRSSリーダーでRSSフィードやAtomフィード、JSONフィードを検出できるようになります。このメソッドの引数には、リンクの種類(: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のアセットパイプラインを有効にすると、/assets/javascripts/ディレクトリにあるJavaScriptファイルにリンクされます(旧Railsのpublic/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/ディレクトリにあるCSSファイルへのリンクを生成します。このリンクはsprockets gemによって処理されます。スタイルシートファイルは、app/assets、lib/assetsvendor/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によって作成されるリンクには、デフォルトでrel="stylesheet"属性が追加されます。適切なオプション(: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ヘルパーは、指定の動画ファイルにリンクするHTML5 <video>タグを生成します。デフォルトでは、public/videosディレクトリからファイルを読み込みます。
<%= video_tag "movie.ogg" %>
上のコードによって以下が生成されます。
<video src="/videos/movie.ogg" />
image_tagの場合と同様、public/videosディレクトリからの絶対パスや相対パスも指定できます。さらに、image_tagの場合と同様に、size: "#{幅}x#{高さ}"オプションも指定できます。idやclassなどのHTMLオプションも引数の末尾に追加できます。
video_tagには、HTMLオプションハッシュ形式で以下を含む任意の<video> 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は、指定の音声ファイルにリンクするHTML5 <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>
ビューのメインbodyは、常に「名前のない」yieldの位置でレンダリングされます。コンテンツを名前付きyieldの位置でレンダリングするには、content_forメソッドを使います。
新しく生成したアプリケーションには、app/views/layouts/application.html.erbテンプレートの<head>要素内に<%= yield :head %>が含まれます。
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 <script>要素、CSS <link>要素、コンテキスト固有の<meta>要素などの要素を、それ以外の一般的なレイアウトの<head>に挿入する場合にも便利です。
パーシャル(部分テンプレート)は、上と別の方法でレンダリング処理を扱いやすい単位に分割するしくみです。パーシャルを使うと、レスポンスで表示するページの特定部分をレンダリングするコードを別ファイルに切り出せます。
パーシャルをビューの一部としてレンダリングするには、ビュー内で以下のようにrenderメソッドを使います。
<%= render "menu" %>
レンダリングされるビュー内に置かれている上のコードは、その場所で_menu.html.erbという名前のファイルをレンダリングします。パーシャルファイル名の冒頭にはアンダースコア(_)が付いていることにご注意ください。パーシャルをコードから参照するときはアンダースコアを付けませんが、通常のビューファイルと区別するためにアンダースコアをパーシャルファイル名に付けます。これは他のフォルダの下にあるパーシャルを取り込む場合も同様です。
<%= render "application/menu" %>
ビューのパーシャルは、テンプレートやレイアウトと同じテンプレートの継承に依存しているので、上のコードはapp/views/application/_menu.html.erbパーシャルをその位置に取り込みます。
パーシャルの利用方法の1つは、パーシャルをサブルーチンと同様に扱うことです。表示の詳細をパーシャル化してビューから追い出し、コードを読みやすくします。たとえば以下のようなビューがあるとします。
<%= render "application/ad_banner" %> <h1>Products</h1> <p>Here are a few of our fine products:</p> <%# ... %> <%= render "application/footer" %>
上のコードの_ad_banner.html.erbパーシャルと_footer.html.erbパーシャルに含まれるコンテンツは、アプリケーションの多くのページと共有できます。こうすることで、そのセクションの詳細を気にせずにページの開発に集中できます。
本ガイドの直前のセクションで説明したように、yieldはレイアウトを簡潔に保つ上で極めて強力なツールです。yieldは純粋なRubyなので、ほぼどこでも利用できます。たとえばyieldを用いると、同じようなさまざまなリソースで使うフォームのレイアウトをDRYに定義できます。
users/index.html.erb
<%= render "application/search_filters", search: @q do |form| %> <p> Name contains: <%= form.text_field :name_contains %> </p> <% end %>
roles/index.html.erb
<%= render "application/search_filters", search: @q do |form| %> <p> Title contains: <%= form.text_field :title_contains %> </p> <% end %>
application/_search_filters.html.erb
<%= form_with model: search do |form| %> <h1>Search form:</h1> <fieldset> <%= yield form %> </fieldset> <p> <%= form.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_with model: zone do |form| %> <p> <b>Zone name</b><br> <%= form.text_field :name %> </p> <p> <%= form.submit %> </p> <% end %>
上の2つのビューは同じパーシャルをレンダリングしますが、Action Viewのsubmitヘルパーはnewアクションで"Create Zone"を返し、editアクションで"Update Zone"を返します。
ローカル変数を特定の状況に限ってパーシャルに渡すには、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オプションを使うと、以下のようにこのローカル変数にオブジェクトを渡せます。
<%= render partial: "customer", object: @new_customer %>
上のcustomerパーシャルの内側では、customerローカル変数は親のビューの@new_customer変数を指すようになります。
あるモデルのインスタンスをパーシャルでレンダリングする場合は、以下のショートハンド記法を利用できます。
<%= render @customer %>
上のコードの@customerインスタンス変数にCustomerモデルのインスタンスが含まれていると仮定すると、_customer.html.erbパーシャルを用いてレンダリングします。このパーシャルに渡した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) || "製品はありません。" %>
パーシャル内で独自のローカル変数を使いたい場合は、:asオプションを指定してパーシャルを呼び出します。
<%= render partial: "product", collection: @products, as: :item %>
上のように変更することで、itemという名前のローカル変数で@productsコレクションのインスタンスにアクセスできるようになります。
locals: {}オプションを使うと、レンダリングするパーシャルに任意のローカル変数を渡せます。
<%= render partial: "product", collection: @products, as: :item, locals: {title: "Products Page"} %>
上の場合、titleという名前のローカル変数に"Products Page"という値が含まれており、パーシャルからこの値にアクセスできるようになります。
コレクションによって呼び出されるパーシャル内では、カウンタ変数も利用できます。このカウンタ変数は、パーシャル名の末尾に_counterを追加した名前になります。
たとえば、パーシャル内で@productsコレクションをレンダリングするときに、_product.html.erbでproduct_counter変数にアクセスできます。この変数は、パーシャルを囲むビューの中でレンダリングされた回数を示すもので、最初のレンダリングでは値が0から始まります。
# index.html.erb <%= render partial: "product", collection: @products %>
# _product.html.erb <%= product_counter %> # 最初のproductは0、次のproductは1...
なお、これはas:オプションでパーシャル名を変更した場合にも該当します。たとえば上のコードのカウンタ変数はitem_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" %> <%= yield :head %> </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 :head do %> <style> #top_menu {display: none} #right_menu {float: right; background-color: yellow; color: black} </style> <% 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タグ内に右メニューが新しく追加されます。
この手法を用いる別のサブテンプレートでも同様の結果を得る方法はいくつも考えられます(ネストのレベルに制限はありません)。ActionView::renderメソッドをrender template: 'layouts/news'経由で使うと、Newsレイアウトで新しいレイアウトがベースになります。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ガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。