269: テンプレートの継承
Other formats:
Rails 3.1の新機能のひとつがテンプレートの継承です。この機能はビューの書き方に革命を起こすというほどではないですが、とても便利な新機能です。今回のエピソードでそのしくみを紹介していきます。
このエピソードのために、各ページに同じナビゲーションを持つアプリケーションを準備しました。
このナビゲーションは今はアプリケーションのレイアウトファイルのdiv要素内にsideというidで、静的に定義されています。
/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Store</title>
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<div id="container">
<% flash.each do |name, msg| %>
<%= content_tag :div, msg, :id => "flash_#{name}" %>
<% end %>
<div id="side">
<strong>Pages</strong>
<ul>
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Products", products_path %></li>
</ul>
</div>
<%= yield %>
<div class="clear"></div>
</div>
</body>
</html>
このナビゲーションを、現在のコントローラに応じて変化するようにカスタマイズしたいと思います。これを実現する方法はいくつかありますが、今回はテンプレートの継承を利用します。そのためにまずナビゲーションをsideという部分テンプレート(partial)に切り出します。
/app/layouts/views/application.html.erb
<div id="side"> <%= render "side" %> </div>
この新しい部分テンプレートにアプリケーション内のすべてのコントローラからアクセスできるようにするためには、どこに置けばいいでしょうか? viewsディレクトリ下にsharedというディレクトリを作成してそこに置くか、あるいはlayoutsディレクトリ下に置くこともできますが、Rails 3.1ではviewsディレクトリ下のapplicationディレクトリに置きます。Rails 3.1ではこのディレクトリはデフォルトでは存在しないため、新規に作成します。
/app/views/application/_side.html.erb
<strong>Pages</strong> <ul> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Products", products_path %></li> </ul>
このsideという部分テンプレートがすべてのコントローラから参照可能になります。これは、ビューの継承がコントローラの継承と並行して動作するからです。コントローラはすべてApplicationControllerを継承しているので、テンプレートはapplicationディレクトリから継承されます。
アプリケーションのいずれかのページを読み込むと、ナビゲーションは以前の通りに表示されます。
この部分テンプレートは、アプリケーションのすべてのコントローラにおいて、簡単にオーバーライドできます。方法は、別のコントローラのviewsディレクトリに_side.html.erbという部分テンプレートファイルを作成するだけです。この設定をProductsControllerに対して行うために、追加のリンクを持つ部分テンプレートを作ります。
/app/views/products/_side.html.erb
<strong>Pages</strong> <ul> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Products", products_path %></li> <li><%= link_to "Manage Products", admin_products_path %></li> </ul>
ProductsControllerの下のいずれかのページを再読み込みすると、新しいリンクが表示されます。他のコントローラ下のページを見てみると、以前と同じようにデフォルトのナビゲーションが表示されています。
これで複数のコントローラにまたがって共有されるテンプレートのための標準的な置き場所が決まりました。そのテンプレートをオーバーライドするには、対象となるコントローラのviewsディレクトリに同じ名前のテンプレートを作成します。
この方法は、ネストが深いコンロトーラでも有効です。今回のアプリケーションではAdmin名前空間の下にいくつかのコントローラがあり、それらはすべてBaseControllerを継承しています。
/app/controllers/admin/base_controller.rb
module Admin class BaseController < ApplicationController end end
このBaseビューのテンプレートもオーバーライドできます。上のページで“Manage Products”のリンクをクリックすると、/admin/productsページが表示され、ナビゲーションはデフォルトに戻ります。すべてのadminページのナビゲーションをオーバーライドしたい場合はどうすればいいでしょうか?
/app/views/adminディレクトリの下にはbaseディレクトリがあります。ここにテンプレートを置けば、自動的に他のコントローラに継承されます。そのように設定しつつ、加えて他のサイドテンプレートと区別できるようにリンクを一つ追加します。
/app/views/admin/base/_side.html.erb
<strong>Pages</strong> <ul> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Products", products_path %></li> <li><%= link_to "Manage Products", admin_products_path %></li> <li><%= link_to "Manage Categories", admin_categories_path %></li> </ul>
admin/productsページを再度読み込むと、baseディレクトリのテンプレートが適用されているのがわかります。
このテンプレートがAdmin名前空間のすべてのページで表示されるのは、これらのページがAdmin名前空間のBaseControllerから継承されているからです。
テンプレートの継承は部分テンプレートだけでなく、すべてのビューテンプレートで有効です。app/views/adminフォルダでは、edit.html.erbテンプレートが、categoriesとproductsの両方のディレクトリに存在します。どちらも中身のコードは同じです。
/app/views/admin/categories/edit.html.erb
<h1>Edit</h1> <%= render 'form' %>
これらのテンプレートの一つを上位のbaseディレクトリに移動してもう片方を削除すると、両方のコントローラが新しい場所のテンプレートを継承するようになります。両方のページとも以前と同じように動作します。アプリケーションに管理用ページが大量にある場合に、この機能は非常に便利です。これらのページは、ビューに繰り返しがたくさんある場合も多いため、この方法を用いればこれらのテンプレートをbaseコントローラに抽象化して取り出し、必要であれば特定のコントローラについてはオーバーライドすることができます。
テンプレートのオーバーライドの話題が出たついでに、もうひとつのトリックとして、コントローラではなく引数に応じてテンプレートをカスタマイズする方法を紹介します。 例えば、モバイル版のサイトをmobileサブドメイン下で提供したいとしましょう。ビュー参照パスを使うことで、サブドメインに応じて使われるビューを変えることができます。この機能は、Rails 3.1の新機能ではなくRails 2でも動作するのですが、この話題に関連するのでここで紹介します。
アプリケーションのコントローラに、ビューパス(view path)と呼ばれるものがあります。ビューでcontroller.view_pathsを呼び出すと、そのコントローラのビューパスを見ることができます。
/app/views/admin/base/edit.html.erb
<h1>Edit</h1> <%= render 'form' %> <%= controller.view_paths %>
productの編集ページを再度読み込むと、view_pathsがリスト表示されます。(なお、アプリケーションの開発中にサブドメインを簡単に使えるようにPowウェブサーバを使用しています。)
このコントローラはデフォルトでビューパスを一つ持っていますが、更に追加できます。お勧めするのはApplicationControllerのbefore_filterを使用する方法です。
/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :subdomain_view_path
def subdomain_view_path
if request.subdomain.present?
prepend_view_path "app/views/#{request.subdomain}_subdomain"
end
end
end
before_filterのメソッドで、サブドメインがある場合には、prepend_view_pathでリクエストのサブドメインに応じて新しいビューパスを追加します。ページを再度読み込むと、この新しいビューパスを見ることができます。
mobileサブドメインがview_pathsの一覧に表示されています。上のURLからmobileサブドメインを削除してページを再読込みすると、表示から消えます。これを使って現在のサブドメインに応じたビューをカスタマイズできます。viewsの下にmobile_subdomainという新しいディレクトリを作成すれば、対応するデフォルトテンプレートをオーバーライドするビューテンプレートをそこに追加できます。ここに新しく作ったproductsディレクトリの下にindex.html.erbテンプレートを新しく作成すると、このテンプレートはmobileサブドメインの下で表示された時だけ使われます。
/app/views/mobile_subdomain/products/index.html
<h1>mobile version</h1>
サブドメインなしでこのページを表示すると、デフォルトのページが表示されます
しかしmobileサブドメインを追加すると、デフォルトページがオーバーライドされるので新しいテンプレートが使われます。
テンプレートのオーバーライドについての今回のエピソードは以上です。継承とビューの参照パスによる2つの方法を紹介しました。どちらも高度なカスタマイズが可能で、ビューを抽象化してオーバーライドする方法を提供します。ビューを更にカスタマイズする必要があればview resolverを作ることもできますが、それはまた別の機会に紹介します。



