Railsのif文で荒れたViewを整理する強力かつ柔軟なテクニック

こんにちは、TECH DRIVEのTedです。

今回は複雑になりがちなViewをスッキリさせることができる強力なテクニックをご紹介します。

ページ単位で部分的に表示を変えたい時や、特定の条件の場合だけ表示したいというようなViewでの複雑な要件に出会うことがあると思います。

そんな時、Viewにif文のような条件式を実装していくと、プロジェクトが進行していく毎にコードの保守性や可読性が落ちやすくなってきます。

今回ご紹介する方法は複雑なViewに対して柔軟な対応ができるようになります。

Viewを整理するテクニックはたくさんあれど、「こういう方法もある」という道具の使い方のご紹介に焦点を置いてご紹介しようと思います。

今回の内容は説明が難しいこともありますので、サンプルコードを用意しました。この記事の最後に掲載させていただきましたので、一通り等記事を読んでからサンプルコードを読んでいただければより理解が深まると思います。

何を使うのか?

Railsが提供しているcontent_forlayoutを使用し、Viewを整理するテクニックをご紹介します。

この2つは柔軟なViewを作るうえで強力な組み合わせになります。

まずはcontent_forlayoutの簡単な説明をしておきます。

content_for

content_forに渡した名前付きブロックは、名前を指定してyieldで呼び出すことができます。

例えばこんな感じでユーザー一覧の画面を出すためのViewがあるとします。 RailsサーバーのUsersController#indexにアクセスしているイメージです。

views/users/index
  
<% content_for :add_content do %>
  # 埋め込みたいコンテンツ
<% end %>
# ユーザー一覧表示の処理が続く
layouts/application.html.erb
# 別の処理

<%= yield %> # users/indexを呼び出す

<% if content_for? :add_content %>
  <%= yield :add_content %> # content_for :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でコントローラーのクラスが継承されている前提で進めます。

  1. dashboard.html.erbでメインのコンテンツをcontent_forブロック内に構築する

  2. dashboard.html.erbにはcontent_forのブロックの後でrender template: 'layouts/application'をテンプレートファイルとして呼び出す

  3. application.html.erbではdashboard.html.erbで構築したコンテンツを<%= yield %>で埋め込む

こうすることで最終のレイアウトはapplication.html.erbとして共通化することができ、レイアウトファイルが増えてもheadタグ等をなんども書く必要が無くなります。

すごくややこしいことをしているように見えるかもしれませんが以下に図を示します。

コントローラーの継承関係とレイアウトファイルの有無 f:id:travy:20190607174521p:plain

コンテンツ生成 f:id:travy:20190607180516p:plain

ファイル数は増えてしまいますが、Viewがグッと読みやすくなりかつ、柔軟性も高まるはずです。

ぼんやりとこんなイメージ感を持って読み進めていただけると良いかもしれません。

f:id:travy:20190617192434p:plain

それでは、順を追って説明します。

先ほどご紹介したコントローラーの継承関係の場合で、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.erbtemplateオプションで呼び出します。

templateオプションをつけることで対象のファイルをレイアウトとして呼び出すことができます。

これでapplication.html.erbが最終的なレイアウトファイルになり、このファイルの中でdashboard.html.erb内で構築したコンテンツを<%= yield :content %>を使って埋め込みます。

# application.html.erb

<% if content_for? :content %>
  <%= yield :content %>
<% else %>
  <%= yield %>
<% end %>

content_for :contentの有無で呼び出すコンテンツを切り替えておくと、今回場合でDashboardControllerを継承しない場合でも対応できるようになります。

これで、application.html.erbでは表示するコンテンツの中身を意識することなく、呼び出すコンテンツ名だけを指定するだけで良いのでif文の中身がスッキリします。

次に、ユーザー詳細画面ではサイドバーを表示したくなったとします。

ここでdashboard.html.erbの中にアクションがshowだった場合のif文かいてその中に、サイドバーのコンテンツを用意して、、、

# dashboard.html.erb

<% 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 %>

# userの詳細の表示が続く

これでユーザーの詳細画面でサイドバーのコンテンツを埋め込むことができます。

他のページ、例えばユーザーの編集画面でも同様にサイドバーが表示したければ

<%= content_for :side_bar do %>
  # サイドバーのコンテンツ
<% end %>

# userの編集画面

とすれば実装可能です。

ページ単位で表示するコンテンツを1つのhtmlファイルにまとめることができるので可読性がグッと上がります。サイドバーのコンテンツが同じものならパーシャル化してしまうのも良いでしょう。

さらにはこんなこともできます。

application.html.erbで書いたようなコンテンツの呼び出し方をdashboard.html.erbでも書いてみます。

<% if content_for? :main_content %>
  <%= yield :main_content %>
<% else %>
  <%= yield %>
<% end %>

そして、ユーザーのリソースでのみ特別なコンテンツを表示したいとします。

# layouts/user.html.erb
<% content_for :main_content do %>
  # ユーザーコンテンツのみの特別なコンテンツ
    
  # ↓それぞれのアクションでのhtmlのコンテンツが埋め込まれる
  <%= yield %>
<% end %>

<%= render template: "layouts/dashboard" %>

図で表すとこんな感じ

f:id:travy:20190617190320p:plain

UsersControllerのアクションにアクセスする場合、検索ルールにしたがってlayouts/user.html.erbがレイアウトファイルとして選択されることになります。

このレイアウトファイルにはcontent_for :main_contentが用意されていて、各アクション(show.html.erbとかindex.html.erbとか)で生成されるコンテンツが<%= yield%>に埋め込まれています。

そして今度は、<%= render template: "layouts/dashboard" %>を呼び出しています。

こうすることでlayouts/user.html.erbdashboard.html.erbを、dashboard.html.erbapplication.html.erbをレイアウトとして呼び出してる状態になります。

結局、先ほどご紹介したように常にapplication.html.erbが最終的なレイアウトファイルになります。今回ご紹介した内容は最終のレイアウトファイルにどんなコンテンツを埋め込むかと考えるとわかりやすいかもしれません。

と、このようにcontent_foryieldを上手に組み合わせるとcontent_forの呼び出し元のコードを汚さなくてよくなりますしコンテンツを柔軟に埋め込むこともできます。

最後に、今回の内容に焦点を当てたサンプルコードを掲載しておきますので手元にcloneしてお試しください。

github.com

付録

今回ご紹介した内容は、柔軟かつ読みやすくhtml要素を埋め込むことができることを前提に書いていますが、同様にやればcssjavascriptを特定のページだけに埋め込むこともできます。

その際は、stylesheet_link_tagjavascript_include_tagを使用して同様に書きます。

  • CSS
# dashboard用のcssファイルをデフォルトのファイルの代わりに使用する
<%= 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 %>
  • JavaScript
# dashboard用のjsファイルをデフォルトのファイルの代わりに使用する
<%= content_for :load_scripts do %>
  <%= javascript_include_tag 'dashboard' %>
<% end %>
<% if content_for? :load_scripts %>
  <%=  yield :load_scripts %>
<% else %>
  <%= javascript_include_tag  [デフォルトのjavascriptファイル] %>
<% end %>

まとめ

もう一度簡単におさらいしておきます。

  • content_for
  • layout

この2つ+render template:を組み合わせることで柔軟なViewの表現ができます。

あとはコンテンツ内容が同じ(サイドバーとか)だけども呼び出す条件だけ違う場合は、そこだけパーシャル化してしまうと可動性、保守性がよくなります。

初めてこれを使ったり、読んだりする人にはすぐには理解が追いつかない内容だと思います。

私がまさにそうでしたw

しかし、今回の内容を自分で作っているサービスで使い倒してみたところ、非常に使いやすくて重宝しています。今回のような少し難しめの内容の場合、頭だけで理解しようとせずに小規模で良いので自分で書いてみると理解が早いかも知れません。

PR

TECH DRIVE協賛企業のサークルアラウンド株式会社では、プログラマーの成長を加速させるためのトレーニングを行なっています。フロントエンド/バックエンド問わず各種バリエーションがございますので、ご興味がある方は是非以下のリンクより詳細をご覧ください。

Ruby Climbing

週1からはじめられる「Ruby」でWEB開発の基礎が習得できる塾です。現役のプログラミング講師&Rubyエンジニアがプログラミング入門からフレームワーク(Sinatra/Ruby on Rails)を使用した本格的なWEB開発の学習までをしっかりとサポートします。

ruby climbing

個別トレーニング

短期間でぐっと成長したい方は弊社主催の個別トレーニングがおすすめです。 トレーニング内容は、受講者の方の課題/要望をお伺いした上で、フルオーダメイドで作成させていただきます。 詳細は以下のリンクよりご確認ください。(応募者多数の場合には時間を別途ご用意する予定です)。

WEBプログラミング個別トレーニング

TECH DRIVEについて

TECH DRIVEは「技術者の成長を加速させる」をキーワードに都内で活動をしているコミュニティです。
TwitterやFacebookにて技術ネタやイベント情報の発信を行っていますので、ご興味があれば、いいねやフォローをお願いいたします。