homeASCIIcasts

269:  テンプレートの継承 

(view original Railscast)

Other translations: En It Es

Other formats:

Written by Naomi Fujimoto

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の下のいずれかのページを再読み込みすると、新しいリンクが表示されます。他のコントローラ下のページを見てみると、以前と同じようにデフォルトのナビゲーションが表示されています。

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ディレクトリのテンプレートが適用されているのがわかります。

BaseControllerから継承しているページは、baseディレクトリのテンプレートを使う

このテンプレートがAdmin名前空間のすべてのページで表示されるのは、これらのページがAdmin名前空間のBaseControllerから継承されているからです。

テンプレートの継承は部分テンプレートだけでなく、すべてのビューテンプレートで有効です。app/views/adminフォルダでは、edit.html.erbテンプレートが、categoriesproductsの両方のディレクトリに存在します。どちらも中身のコードは同じです。

/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ウェブサーバを使用しています。)

ビューパスが表示されたページ

このコントローラはデフォルトでビューパスを一つ持っていますが、更に追加できます。お勧めするのはApplicationControllerbefore_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サブドメインを追加すると、デフォルトページがオーバーライドされるので新しいテンプレートが使われます。

mobileサブドメイン下でページを見ると、mobileテンプレートが表示される

テンプレートのオーバーライドについての今回のエピソードは以上です。継承とビューの参照パスによる2つの方法を紹介しました。どちらも高度なカスタマイズが可能で、ビューを抽象化してオーバーライドする方法を提供します。ビューを更にカスタマイズする必要があればview resolverを作ることもできますが、それはまた別の機会に紹介します。