こんにちは、TECH DRIVEのTedです。
今回は複雑になりがちなViewをスッキリさせることができる強力なテクニックをご紹介します。
ページ単位で部分的に表示を変えたい時や、特定の条件の場合だけ表示したいというようなViewでの複雑な要件に出会うことがあると思います。
そんな時、Viewにif文のような条件式を実装していくと、プロジェクトが進行していく毎にコードの保守性や可読性が落ちやすくなってきます。
今回ご紹介する方法は複雑なViewに対して柔軟な対応ができるようになります。
Viewを整理するテクニックはたくさんあれど、「こういう方法もある」という道具の使い方のご紹介に焦点を置いてご紹介しようと思います。
今回の内容は説明が難しいこともありますので、サンプルコードを用意しました。この記事の最後に掲載させていただきましたので、一通り等記事を読んでからサンプルコードを読んでいただければより理解が深まると思います。
何を使うのか?
Railsが提供しているcontent_for
とlayout
を使用し、Viewを整理するテクニックをご紹介します。
この2つは柔軟なViewを作るうえで強力な組み合わせになります。
まずはcontent_for
とlayout
の簡単な説明をしておきます。
content_for
content_for
に渡した名前付きブロックは、名前を指定してyieldで呼び出すことができます。
例えばこんな感じでユーザー一覧の画面を出すためのViewがあるとします。
RailsサーバーのUsersController#indexにアクセスしているイメージです。
views/users/index
<% content_for :add_content do %>
<% end %>
layouts/application.html.erb
<%= yield %>
<% if content_for? :add_content %>
<%= yield :add_content %>
<% end %>
上のコードの場合、<% yield %>
で呼び出したusers/index.html.erb
を呼び出し、さらに呼び出したusers/index.html.erb
の中にcontent_for :add_content
があるので<% yield :add_content %>
でその中身を呼び出す流れです。
yield
に続けてシンボル名を記述すると、そのシンボル名に対応する content_for
のブロックの中身をそこへ埋め込んでくれます。
content_for
のみではそこまでの大きな利点がないかもしれません。一定の単位で、表示するコンテンツを切り替えたい時なんかに真価を発揮するようになります。
次にご紹介するlayoutを組み合わせると、ここまでの説明の意味がわかるようになるはずです。
layout
こちらはコントローラー内で指定するlayout
のことを指します。
これは、どのファイルをViewのレイアウトファイルとして使用するかを定義することができます。
rails newしたときにデフォルトで使用されるレイアウトファイルはapp/views/layouts/appication.html.erb
です。
Railsは使用するViewのレイアウトファイルを検索するときに、呼び出されているコントローラーの基本名をもつレイアウトファイルがapp/views/layouts/
以下にあることを探すというルールがあります。
例えばUsersControllerの場合はView/layouts/users.html.erb
を探しにいきます。
該当ファイルがなければ、デフォルトで用意されているapp/views/layouts/application.html.erb
を使用してくれます。
ここで、コントローラー内でlayout
を指定するとレイアウトファイルを検索するルールを上書きすることになります。
例えば以下の感じで指定するとUsersController
のアクションにアクセスする場合、app/views/layouts/main.html.erb
があるかどうかを探しに行き、ある場合はレイアウトファイルとして使用します。
class UsersController < ApplicationController
layout "main"
def index
end
end
こうすればコントローラー単位で使用するレイアウトを切り替えることができそうです。
実は、レイアウトファイルを検索するルールには続きがあります。
それは、呼び出されているコントローラーの基本名をもつレイアウトファイルがない場合は、継承元のコントローラーの基本名、もしくはlayoutで指定したレイアウトファイルがあるかどうかを次に検索します。
通常のRailsの場合、コントローラーはApplicationController
を継承しているはずなので、最終的な継承元であるApplicationController
の基本名のapplication.html.erb
に行き着くので、先ほどご紹介したapplication.thml.erb
がデフォルトのレイアウトであるということは、これに倣っているということになります。
呼び出すコントローラーとApplicationController
の間に別のコントローラーを継承するとそのコントローラーの階層でもレイアウトファイルの検索をしてくれます。
class DashboadController < ApplicationController
end
class UsersController < DashboadController
end
この場合、users -> dashboard -> application の順でレイアウトファイルを検索してくれます。
DashboardController
を継承するかどうかで使用するレイアウトファイルを切り替えられそうです。
何をどうするか
Users < Dashboard < Application
でコントローラーのクラスが継承されている前提で進めます。
dashboard.html.erbでメインのコンテンツをcontent_for
ブロック内に構築する
dashboard.html.erbにはcontent_for
のブロックの後でrender template: 'layouts/application'
をテンプレートファイルとして呼び出す
application.html.erbではdashboard.html.erbで構築したコンテンツを<%= yield %>で埋め込む
こうすることで最終のレイアウトはapplication.html.erbとして共通化することができ、レイアウトファイルが増えてもheadタグ等をなんども書く必要が無くなります。
すごくややこしいことをしているように見えるかもしれませんが以下に図を示します。
コントローラーの継承関係とレイアウトファイルの有無
コンテンツ生成
ファイル数は増えてしまいますが、Viewがグッと読みやすくなりかつ、柔軟性も高まるはずです。
ぼんやりとこんなイメージ感を持って読み進めていただけると良いかもしれません。
それでは、順を追って説明します。
先ほどご紹介したコントローラーの継承関係の場合で、UserControllerのindexアクションにアクセスするとします。
class DashboadController < ApplicationController
end
class UsersController < DashboadController
def index
end
end
DashboadController
の基本名であるdashboard.html.erb
を、app/views/layouts/
以下に作っておいてレイアウトファイルとして使います。
ここで一捻り加えます。
dashboard.html.erb
内ではhtmlのbodyタグに配置するべき内容を全ての内容を content_for :content
ブロック内に配置します。
<% content_for :content do %>
<header>
# ヘッダーに表示したい情報
</header>
<main>
# サイドバーとか
<%= render 'layouts/sidebar' %>
# flashメッセージとか
<%= render 'layouts/flash' %>
# ↓メインコンテンツ
<%= yield %>
</main>
<% end %>
しかし、これだけでは画面に何も表示されません。content_for
で登録したブロックをyieldで呼び出す必要があります。
メインコンテンツ
直下の<%= yield %>にアクセス中のアクションで呼ばれているusers/index.html.erb
の内容が埋め込まれます。
何も表示されないと困るので、上記のコードの最後に次の1行を加えます。
<%= render template: "layouts/application" %>
デフォルトで用意されているapplication.html.erb
を template
オプションで呼び出します。
template
オプションをつけることで対象のファイルをレイアウトとして呼び出すことができます。
これでapplication.html.erb
が最終的なレイアウトファイルになり、このファイルの中でdashboard.html.erb内で構築したコンテンツを<%= yield :content %>
を使って埋め込みます。
<% if content_for? :content %>
<%= yield :content %>
<% else %>
<%= yield %>
<% end %>
*content_for :content
の有無で呼び出すコンテンツを切り替えておくと、今回場合でDashboardController
を継承しない場合でも対応できるようになります。
これで、application.html.erb
では表示するコンテンツの中身を意識することなく、呼び出すコンテンツ名だけを指定するだけで良いのでif文の中身がスッキリします。
次に、ユーザー詳細画面ではサイドバーを表示したくなったとします。
ここでdashboard.html.erb
の中にアクションがshowだった場合のif文かいてその中に、サイドバーのコンテンツを用意して、、、
<% if controller.controller_name = "Users" && controller.action_name == "show" %>
<% end %>
としなくても良いのです。dashboard.html.erb
にはサイドバーのコンテンツを実際に書く必要はなく、先ほどと同じ要領で
<% if content_for? :side_bar %>
<%= yield :side_bar %>
<% end %>
と書いておけば良いのです。dashboard.html.erb
は
「:side_bar
のコンテンツがあったら埋め込んでやるよ」くらいの気構えです。
そしてサイドバーを出したいusers/show.html.erb
でサイドバーの内容をcontent_for :side_bar
のブロック内に用意します。
<%= content_for :side_bar do %>
<% end %>
これでユーザーの詳細画面でサイドバーのコンテンツを埋め込むことができます。
他のページ、例えばユーザーの編集画面でも同様にサイドバーが表示したければ
<%= content_for :side_bar do %>
<% end %>
とすれば実装可能です。
ページ単位で表示するコンテンツを1つのhtmlファイルにまとめることができるので可読性がグッと上がります。サイドバーのコンテンツが同じものならパーシャル化してしまうのも良いでしょう。
さらにはこんなこともできます。
application.html.erbで書いたようなコンテンツの呼び出し方をdashboard.html.erbでも書いてみます。
<% if content_for? :main_content %>
<%= yield :main_content %>
<% else %>
<%= yield %>
<% end %>
そして、ユーザーのリソースでのみ特別なコンテンツを表示したいとします。
<% content_for :main_content do %>
<%= yield %>
<% end %>
<%= render template: "layouts/dashboard" %>
図で表すとこんな感じ
UsersControllerのアクションにアクセスする場合、検索ルールにしたがってlayouts/user.html.erbがレイアウトファイルとして選択されることになります。
このレイアウトファイルにはcontent_for :main_content
が用意されていて、各アクション(show.html.erbとかindex.html.erbとか)で生成されるコンテンツが<%= yield%>
に埋め込まれています。
そして今度は、<%= render template: "layouts/dashboard" %>
を呼び出しています。
こうすることでlayouts/user.html.erb
がdashboard.html.erb
を、dashboard.html.erb
がapplication.html.erb
をレイアウトとして呼び出してる状態になります。
結局、先ほどご紹介したように常にapplication.html.erb
が最終的なレイアウトファイルになります。今回ご紹介した内容は最終のレイアウトファイルにどんなコンテンツを埋め込むかと考えるとわかりやすいかもしれません。
と、このようにcontent_for
とyield
を上手に組み合わせるとcontent_for
の呼び出し元のコードを汚さなくてよくなりますしコンテンツを柔軟に埋め込むこともできます。
最後に、今回の内容に焦点を当てたサンプルコードを掲載しておきますので手元にcloneしてお試しください。
github.com
付録
今回ご紹介した内容は、柔軟かつ読みやすくhtml要素を埋め込むことができることを前提に書いていますが、同様にやればcss
やjavascript
を特定のページだけに埋め込むこともできます。
その際は、stylesheet_link_tag
やjavascript_include_tag
を使用して同様に書きます。
<%= content_for :styles do %>
<%= stylesheet_link_tag 'dashboard', media: 'all' %>
<% end %>
<% if content_for? :styles %>
<%= yield :styles %>
<% else %>
<%= stylesheet_link_tag [デフォルトのcssファイル] , media: 'all' %>
<% end %>
<%= content_for :load_scripts do %>
<%= javascript_include_tag 'dashboard' %>
<% end %>
<% if content_for? :load_scripts %>
<%= yield :load_scripts %>
<% else %>
<%= javascript_include_tag [デフォルトのjavascriptファイル] %>
<% end %>
まとめ
もう一度簡単におさらいしておきます。
この2つ+render template:
を組み合わせることで柔軟なViewの表現ができます。
あとはコンテンツ内容が同じ(サイドバーとか)だけども呼び出す条件だけ違う場合は、そこだけパーシャル化してしまうと可動性、保守性がよくなります。
初めてこれを使ったり、読んだりする人にはすぐには理解が追いつかない内容だと思います。
私がまさにそうでしたw
しかし、今回の内容を自分で作っているサービスで使い倒してみたところ、非常に使いやすくて重宝しています。今回のような少し難しめの内容の場合、頭だけで理解しようとせずに小規模で良いので自分で書いてみると理解が早いかも知れません。
PR
TECH DRIVE協賛企業のサークルアラウンド株式会社では、プログラマーの成長を加速させるためのトレーニングを行なっています。フロントエンド/バックエンド問わず各種バリエーションがございますので、ご興味がある方は是非以下のリンクより詳細をご覧ください。
Ruby Climbing
週1からはじめられる「Ruby」でWEB開発の基礎が習得できる塾です。現役のプログラミング講師&Rubyエンジニアがプログラミング入門からフレームワーク(Sinatra/Ruby on Rails)を使用した本格的なWEB開発の学習までをしっかりとサポートします。
個別トレーニング
短期間でぐっと成長したい方は弊社主催の個別トレーニングがおすすめです。
トレーニング内容は、受講者の方の課題/要望をお伺いした上で、フルオーダメイドで作成させていただきます。
詳細は以下のリンクよりご確認ください。(応募者多数の場合には時間を別途ご用意する予定です)。