Ruby on Rails : ページの外見をカスタマイズ(前編)
さあ、 Ruby on Rails ネタの続きです。前回までで記事データの入力とカテゴリー分けまでは実装できましたので、本日はテンプレートを使ってページの外見をカスタマイズしようと思います。テンプレートはこれまでも少し変更してきましたが、今回はその仕組みについてきちんとご紹介し、その後で実例をお見せします。今回も僅かな記述量で大きな効果が得られる Ruby on Rails の特徴がおおいに発揮されていますので、ぜひ読んでみてください。
概要
Ruby on Rails は MVC アーキテクチャに基づいて設計されていますので(MVC アーキテクチャについてはこちらの記事をご覧ください )、ページの HTML を実際に作成するのはビューの役割です。 そして Ruby on Rails でのビューとは、 "app/views" 以下に配置されている eRuby スクリプト(".rhtml" ファイル)そのものです。 実は eRuby 以外の方法でビューを実装することもできますが、それは後々ご紹介することにして、ここでは eRuby によるビューのみを考えることにします。
ビューの構成要素
通常、 Ruby on Rails が出力するページは複数の eRuby スクリプトを実行して生成されます。それらの eRuby スクリプトは、大別して以下の 3 種類に分類できます。
種類 | 主にレンダリングする内容 |
---|---|
テンプレート | モデルの内容を反映した各ページ固有の内容 |
部分テンプレート | 複数のページで使いまわされる内容 |
レイアウト | 各ページの共通部分・全体のレイアウト |
要するに全ページ(実際にはコントローラごと)共通の内容は「レイアウト」、ある一部のページで使いまわされる内容は「部分テンプレート」、ページごとに固有の内容は「テンプレート」にそれぞれ記述すると考えると良いでしょう。もちろん強制ではないので、ケースバイケースで柔軟に使い分けることができます。
それでは、上記の 3 種類について、個別に詳細をご紹介しようと思います。
テンプレート
テンプレートはページの主な内容を生成するための eRuby スクリプトです。通常、コントローラのそれぞれのアクションに対して個別のテンプレートを用意します。
アクションメソッド内でなにも指定しなければ、コントローラはデフォルトで "app/views/<コントローラ名>/<アクション名>.rhtml" という eRuby スクリプトをテンプレートとして使用します。アクションメソッド内で render メソッドを呼び出せば、使用するテンプレートを変更することも可能です。この場合の render メソッドには、以下の 3 つの呼び出し方法があります。
- render(:action ⇒ '<アクション名>')
- 同じコントローラのアクション名で指定したアクションメソッドのデフォルトテンプレートを使用します。
- render(:template ⇒ '<コントローラ名>/<アクション名>')
- コントローラ名で指定したコントローラのアクション名で指定したアクションのデフォルトテンプレートを使用します。
- render(:file ⇒ '<パス名>' [,:use_full_path ⇒ true])
- テンプレートとして使用する eRuby ファイルを直接指定します。デフォルトではパス名はフルパスでなければなりませんが、 ":use_full_path ⇒ true" を同時に指定すると "app/views" からの相対パスになります。
テンプレート内では、以下の変数やメソッドが使用できます。
- コントローラの全てのインスタンス変数
- headers, params, request, response, session の各アクセスメソッド
- 現在のコントローラ(controller という変数名でアクセスできます)
- テンプレートのベースディレクトリのパス(base_path という変数名でアクセスできます)
ただし、テンプレートに含める Ruby コードは最小限にとどめたほうが無難です。ロジック部分までビューに記述してしまうと、 MVC の分離が崩れて再利用性やメンテナンス性を落とすことになります。ビューはあくまでコントローラで生成した結果を HTML などに整形するだけ、と考えておくべきです。
部分テンプレート
テンプレートで各ページの表示はカスタマイズできますが、このままでは似たような記述がいくつものテンプレートに散らばってしまう可能性があります。例えば、データベース項目の新規作成ページと編集ページはどちらも似たような入力フォームを含んでいるでしょう。このままでは、Ruby on Rails の原則のひとつである DRY(Don't Repeat Yourself:繰り返しを避ける)に反してしまいます。
この問題に対する答えが、「部分テンプレート」です。これは、他のテンプレートの途中に挿入するための特別なテンプレートです。 Scaffold が作成したビューの入力フォームも部分テンプレートになっています("app/views/<コントローラ名>/_form.rhtml")。部分テンプレートの挿入は、テンプレート内で render メソッドを呼び出すことによって行います。render メソッドのパラメータには、以下のシンボルのものが指定できます。
- :partial
- 必須のパラメータで、挿入する部分テンプレートのファイル名(拡張子なし)を指定します。実際に挿入されるファイルはここで指定した名前の先頭にアンダースコア "_" を付けたものです。つまり、部分テンプレートのファイル名は常にアンダースコアで始まらなければなりません。同時に、 Ruby の識別子として有効な名前でなければなりません。
- :object
- 部分テンプレートに引き渡すオブジェクトを指定します。そのオブジェクトは、部分テンプレートの中では :partial に指定した文字列と同じ名前のローカル変数としてアクセスできます。 :collection と同時には指定できません。
- :collection
- このパラメータにオブジェクトの配列(もしくは他の列挙可能なオブジェクト)を指定すると、そのすべての要素に対して render メソッドを呼んだのと同じ結果になります。このとき、該当するオブジェクトは :object と同じ方法で渡され、さらにインデックスが <:partial に指定した名前>_conter というローカル変数も定義されます。 :object と同時には指定できません。
- :spacer_template
- :collection を指定したときに、各要素の間に挿入される部分テンプレートを指定します。ここでも、実際に検索されるのは指定した名前の先頭にアンダースコアを付けたファイル名です。
- :locals
- 部分テンプレートのローカル変数を定義するハッシュを指定します。
上記以外にも、親のテンプレートでアクセス可能なデータ(ローカル変数除く)にはすべてアクセス可能です。ただし、親で定義された変数名に依存することになるので、再利用性はかなり落ちてしまいます。なるべく上記のパラメータ経由でデータを引き渡すようにしたほうがよいでしょう。具体的な記述方法は後にご紹介する実例をご参照いただきたいのですが、基本的には通常のテンプレートとほとんど変わりません。部分テンプレート内でさらに別の部分テンプレートを挿入することも可能です。
部分テンプレートファイルは "app/views/<コントローラ名>/" から検索されます。ただし、 ;partial や :spacer_template の値に "/" が含まれる場合、それは "app/views" からの相対パスとみなされます。この指定方法は複数のコントローラで部分テンプレートを共有する場合に役立ちます。強制ではありませんが、そういった共有部分テンプレートは "app/views/shared" に配置されることが多いようです。実際に検索されるファイルはファイル名の先頭(最後の "/" の直後)にアンダースコアが付加されることを忘れないでください。
レイアウト
レイアウトは通常コントローラごとに用意され、各ページの共通部分の生成やおおまかな配置を記述します。以下は Scaffold によって生成された blognavi の article コントローラのレイアウトです。
<html> <head> <title>Article: <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %> </head> <body> <p style="color: green"><%= flash[:notice] %></p> <%= @content_for_layout %> </body> </html>
やっていることはだいたいわかると思いますが、 @content_for_layout というインスタンス変数にテンプレートの処理結果が格納されており、それを HTML に挿入しています。このことから、レイアウトのスクリプトが実行されるのはテンプレートが実行された後だということがわかりますね。そのほか、レイアウトではテンプレートでアクセス可能な全てのインスタンス変数・メソッドにアクセスできます。テンプレートで代入したインスタンス変数にもアクセスできますので、それを使ってテンプレートからレイアウトにデータを渡すことも可能です。
レイアウトとして使用する eRuby スクリプトは、通常 "app/views/layouts/<コントローラ名>.rhtml" となります。もしこのファイルが存在しない場合、 "app/views/layouts/application.rhtml" が使用されます。サイト内のすべてのページでレイアウトを統一するのに便利ですね。さらに、コントローラのコード内でレイアウトのファイルを指定する方法もいくつか存在します。もっとも簡単なのはやはり render メソッドで指定する方法ですね。以下の 2 つの呼び方があります。
- render(:layout ⇒ 'スクリプト名')
- "app/views/layouts/<スクリプト名>.rhtml" をレイアウトとして使用します。
- render(:layout ⇒ false)
- レイアウトを使用しません(テンプレートの処理結果をそのまま出力します)。
その他にも、コントローラのクラス定義時に指定する方法や、同じくコントローラの determine_layout メソッドで指定する方法などもあるようです。
実例
それでは、今回ご紹介した知識を使って blognavi のページをカスタマイズしてみましょう。例として、記事のリスト表示とカテゴリーの詳細表示のページを題材にします。
記事リストに該当記事へのリンクを追加
現在、記事リストには記事の ID とタイトル、それに詳細表示、編集、削除の 3 つのリンクがあります。しかし、このままではあまり役に立たないので、タイトルに livedoor blog の該当記事へのリンクを設定し、さらに記事内容の編集画面へのリンクも新設しましょう。ついでに、次にご紹介する「カテゴリーの詳細表示に記事リストを含める」の下ごしらえとして、記事リストの生成を部分テンプレート化しておきます。
まずはその部分テンプレートとして "app/views/article/_list.rhtml" を以下の内容で作成します。 style 要素を直書きしているなど、あまり褒められた書き方ではありませんが、暫定措置ということでご勘弁ください(^^ゞ
<table class="article_list"> <tr> <th style="width:80px;">Number</th> <th>Title</th> <th style="width:220px;">Action</th> </tr> <% for article in list %> <tr> <td><%=h article.number %></td> <td> <%= '<a href="http://webos-goodies.jp/archives/' + h(article.number.to_s) + '.html">' %> <%=h article.title %></a> </td> <td> <%= '<a href="http://cms.blog.livedoor.com/cms/article/edit?blog_id=1427319&id=' + h(article.number.to_s) + '">' %> Edit Article</a> <%= link_to 'Show', :action => 'show', :id => article %> <%= link_to 'Edit', :action => 'edit', :id => article %> <%= link_to 'Destroy', { :action => 'destroy', :id => article }, :confirm => 'Are you sure?' %> </td> </tr> <% end %> </table>
ここで、 article.number を h というメソッドに渡しているのにお気づきかと思います。この h メソッドは HTML の特殊文字をエスケープしてくれるメソッドです。ユーザーが入力した文字列をそのまま HTML に埋め込むと、さまざまな脆弱性の原因になるため、外部由来のデータを表示するときは必ず h メソッドを通すようにしてください。
上記の部分テンプレートを適用するため、記事リストのテンプレートである "app/views/article/list.rhtml" にある記事リストの生成を部分テンプレートの呼び出しに置き換えます。
<h1>Listing articles</h1> <%= render(:partial => "list", :object => @articles) %> <%= link_to 'Previous page', { :page => @article_pages.current.previous } if @article_pages.current.previous %> <%= link_to 'Next page', { :page => @article_pages.current.next } if @article_pages.current.next %> <br /> <%= link_to 'New article', :action => 'new' %>
見た目を整えるため、 CSS も変更しましょう。 Ruby on Rails では CSS は "public/stylesheets" に配置することになっており、 Scaffold で生成したビューはそこにある "scaffold.css" を参照しています。そこで、その "scaffold.css" に以下の内容を追加します。
table.article_list { width: 100%; table-layout: fixed; border-spacing: 6px 0px; } table.article_list th { padding: 0px; text-align: left; border-bottom: 1px solid black; } table.article_list td { padding: 2px 0px 2px 8px; overflow: hidden; white-space: nowrap; }
ここまでの変更で、記事リストのページは以下のようになります。
だいぶマシな表示になってきました。
カテゴリーの詳細表示に記事リストを含める
次は、カテゴリーの詳細表示画面にそのカテゴリーに所属する記事を表示させましょう。先ほど記事リストの表示を部分テンプレートとして実装したのが役に立ちます。しかし、まずはコントローラーに記事リストを取得する処理を追加しましょう。詳細表示を行うのは show アクションですので、 show メソッドに追加します。
class CategoryController < ApplicationController #...中略... def show @category = Category.find(params[:id]) @articles = Article.find(:all, :conditions => ["category_id = ?", params[:id]]) end #...中略... end
find メソッドは以前も使いましたね。今回は第 2 引数に条件を指定しています。 find メソッドの詳細はまた次の機会に譲ることにして、今回はこれで @articles 変数に該当カテゴリに所属する記事リストが格納されることだけ押さえておいてください。
では、ビューに部分テンプレートの呼び出しを追加しましょう。 category コントローラの show メソッドに対応するビューは "app/views/category/show.rhtml" ですので、そこに render メソッドの呼び出しを追加します。ついでに、カテゴリー名の表示も p タグから h1 に変更しておきました(笑)。
<% for column in Category.content_columns %> <h1> <%=h @category.send(column.name) %> </h1> <% end %> <%= render(:partial => "article/list", :object => @articles) %> <br/> <%= link_to 'Edit', :action => 'edit', :id => @category %> | <%= link_to 'Back', :action => 'list' %>
render メソッドに渡している :partial ハッシュの値で部分テンプレートを指定しています。この場合、実際に読まれる部分テンプレートは "app/views/article/_list.rhtml" になります。前述の部分テンプレートの説明をご参照ください。また、コントローラの show メソッドで取得した記事リストを :object を介して部分テンプレートに引き渡しています。この値が部分テンプレート内の変数 list に代入されます。
これらの変更を施したカテゴリー詳細表示ページは以下のようになります。
まだ Ruby on Rails カテゴリの記事しか入力していなかったので、記事リストのページとほとんど見分けがつきませんね(^^ゞ。ま、なんにせよ、記事リストページの表示がそのままこちらにも適用されているのがお分かりいただけるかと思います。 Ruby on Rails は、ほんとに少ない記述量でいろいろなことが実現できますね。
本日は Ruby on Rails でのテンプレートとレイアウトを使用したページのカスタマイズについてご紹介しました。レイアウトを使用した実例をご紹介できなかったので、次回はそのあたりを補足したいと思います。最低限使えるようになるまであと少しですね。頑張ります(^^)。では、また。
詳しくはこちらの記事をどうぞ!
この記事にコメントする