誰が興味があるのか謎ですが、ActionView を単体で使ってみようと思います。 意外にも Rails の仕組みとか見えてくるかもしれません。

Rails 4.1 ぐらいから ActionPack から独立した記憶があります。どうでしたっけ。

テンプレートを使いたい時には erb, haml, slim などを単体で利用すればいいのであまり使う機会はないかもしれません。

雑感では、

  • layout 機能を使いたい
  • インスタンス変数で値にアクセスしたい
  • Rails が提供するビューヘルパーを使いたい

あたりがメリットかと思います。

この記事のために作成したコードはこちらにおいておきます。

補足の部分は読み飛ばせるように書いているつもりです。

利用したRailsのバージョンは 4.1.4 です。

1 Hello, world

まずは使ってみます。

ActionView::Base.new.render(inline: 'Hello, World!') # => Hello, world

ActionView::Base のインスタンスを作成し、renderメソッドを呼びだします。 コントローラでの render メソッドはどうやらこの render メソッドのようです。

(viewで使う render もこの render ですが…)

1の補足 ActionView::Base

Rails を使ってる際に erb ファイルの中で self.class を確認したことはあるでしょうか?

ちょっと確認してみましょう。

<%= self.class %>
<%= self.class.superclass %>
#<Class:0x007f82891092e0>
ActionView::Base

self は無名のクラスになっていますが、そのスーパークラスは ActiovView::Base です。 ビューは ActionView::Base のインスタンスのコンテキストで実行されるわけです。ビューコンテキストと呼んでいるようです。

また、このクラスにヘルパーをミックスインすることでヘルパーとして利用できるようになります。

デフォルトのHelperはすでに include されています。

> ActionView::Base.ancestors.map(&:to_s).grep(/Helper/)
=> ["ActionView::Helpers", "ActionView::Helpers::TranslationHelper", "ActionView::Helpers::RenderingHelper", "ActionView::Helpers::RecordTagHelper", "ActionView::Helpers::OutputSafetyHelper", "ActionView::Helpers::NumberHelper", "ActionView::Helpers::JavaScriptHelper", "ActionView::Helpers::FormOptionsHelper", "ActionView::Helpers::FormHelper", "ActionView::Helpers::FormTagHelper", "ActionView::Helpers::TextHelper", "ActionView::Helpers::DebugHelper", "ActionView::Helpers::DateHelper", "ActionView::Helpers::CsrfHelper", "ActionView::Helpers::ControllerHelper", "ActionView::Helpers::CacheHelper", "ActionView::Helpers::AtomFeedHelper", "ActionView::Helpers::AssetTagHelper", "ActionView::Helpers::AssetTagHelper::StylesheetTagHelpers", "ActionView::Helpers::AssetTagHelper::JavascriptTagHelpers", "ActionView::Helpers::SanitizeHelper", "ActionView::Helpers::ActiveModelHelper", "ActionView::Helpers::UrlHelper", "ActionView::Helpers::TagHelper", "ActionView::Helpers::CaptureHelper"]

2 インスタンス変数を使う

Rails ではコントローラのインスタンス変数がビューの中で使えます。 普段はRailsが自動でやってくれていますが、自分でインスタンス変数を設定するには assign メソッドを使います。

view_context = ActionView::Base.new
view_context.assign(name: 'eiel')
view_context.render(inline: 'Hello, <%= @name %>') # => Hello, eiel

@name が eiel に展開されています。

ActionView::Base のコンストラクタの第2引数に渡しても設定できます。

2の補足 ActionView::Rendering

コントローラがビューコンテキストに対して assign メソッドを利用して、設定します。 これは ActionView::Rendering で行われます。

この ActionView::Rendering には ActionController::Base にミックスインされていて、コントローラがビューを設定する処理などが記述されているようです。

> ActionController::Base.ancestors.map(&:to_s).grep(/ActionView/)
=> ["ActionView::Layouts", "ActionView::Rendering", "ActionView::ViewPaths"]

ActionController::Base には ActionView::Rendering がミックスインされています。

ちなみに assign するのに使う Hash は AbstractController::Rendering#view_assign で作成されています。

def view_assigns
  protected_vars = _protected_ivars
  variables      = instance_variables

  variables.reject! { |s| protected_vars.include? s }
  variables.each_with_object({}) { |name, hash|
    hash[name.slice(1, name.length)] = instance_variable_get(name)
  }
end

インスタンス変数の一覧を取り出し、先頭の @ を取り除いてハッシュにしています。_protected_ivars に登録されているものは除外されます。

3 テンプレートファイルの利用

別のファイルに保存したテンプレートを利用してみます。 ActionView::LookupContext というものがテンプレートファイルを探します。

views/prefix/hoge.html.erb を用意して中身は

Hello, <%= @name %>

として用意しているとします。

使ってみます。

require 'action_dispatch/http/mime_type'
view_context = ActionView::Base.new('./views')
view_context.assign(name: 'eiel')
view_context.render(template: 'hoge', prefixes: 'prefix') # => Hello, eiel

ActionView::Base の第一引数から自動的に ActionView::LookupContext が生成されます。

バグなのかどうか判断が付いていないですが action_dispatch/http/mime_type を読まなりと動いてくれません。

どうしても読みたくない場合は以下のようにします。

lookup_context = ActionView::LookupContext.new('./views')
lookup_context.cache = false   # ActionPack を読まなくて済む魔法

view_context = ActionView::Base.new(lookup_context)
view_context.assign(name: 'eiel')
view_context.render(template: 'hoge', prefixes: 'prefix') # => Hello, eiel

ルックアップコンテキストを自分で作り、cache を切ると ActionDispatch を利用せずに動かすことができます。

Rails が prefixes と template を自動で設定してくれていることが想像できます。普段はコントローラ名やアクション名から判断できるからですね。

prefixes は指定しないとテンプレートをみつけることができないようです。

また、文字列を指定することもできます。

view_context.render('prefix/hoge')

この場合は prefix/_hoge.html.erb のようなファイルを探しにいきます。

3の補足

render に自動設定されるオプションは _normalize_options メソッドで設定されるようです。

例えば ActionVIew::Rendreing#_normalive_options では

def _normalize_options(options)
  options = super(options)
  if options[:partial] == true
    options[:partial] = action_name
  end

  if (options.keys & [:partial, :file, :template]).empty?
    options[:prefixes] ||= _prefixes
  end

  options[:template] ||= (options[:action] || action_name).to_s
  options
end

となっていて prefixes や template が設定されている様子があります。

特に options[:template] ||= (options[:action] || action_name).to_s なんかは予想通りな感じですね。 options に :action を利用して、なければ action_name を利用しています。

prefixes は ActionView::ViewPaths

def local_prefixes
  [controller_path]
end

となっており、

def _prefixes # :nodoc:
  @_prefixes ||= begin
    deprecated_prefixes = handle_deprecated_parent_prefixes
    if deprecated_prefixes
      deprecated_prefixes
    else
      return local_prefixes if superclass.abstract?

      local_prefixes + superclass._prefixes
    end
  end
end

最終的に _prefixes として利用できることがわかります。

そういえば ActionView::ViewPaths も ActionController::Base にミックスインされていましたね。

> ActionController::Base.ancestors.map(&:to_s).grep(/ActionView/)
=> ["ActionView::Layouts", "ActionView::Rendering", "ActionView::ViewPaths"]

4 レイアウトの利用

レイアウトを利用するには layout オプションを使います。

$ tree views/
views/
├── layouts
│   └── application.html.erb
└── prefix
    └── hoge.html.erb
$ cat view/layouts/application.html.erb
--
<%= yield %>
--

としておいて、

lookup_context = ActionView::LookupContext.new('./views')
lookup_context.cache = false   # ActionPachk を読まなくて済む魔法

view_context = ActionView::Base.new(lookup_context)
view_context.assign(name: 'eiel')
view_context.render(template: 'hoge',
                          prefixes: 'prefix',
                          

とすると、

--
Hello, eiel

--

のような文字列がかえってきます。

4の補足

layout に関するコントローラの処理は ActionView::Layouts にあります。

もう一度確認してみましょう。ActionController::Base にミックスインされているモジュールを確認してみましょう。

> ActionController::Base.ancestors.map(&:to_s).grep(/ActionView/)
=> ["ActionView::Layouts", "ActionView::Rendering", "ActionView::ViewPaths"]

ちなみに ActionView::Rendering は ActionView::Layouts で include されています。

render へのオプション設定はやっぱり _normalaize_options にあります。

def _normalize_options(options) # :nodoc:
  super

  if _include_layout?(options)
    layout = options.delete(:layout) { :default }
    options[:layout] = _layout_for_option(layout)
  end
end

options[:layout] を設定しています。

まとめ

ActionView を単体で使いたい場面を考えると ERB を単体で利用していたけど layout を使いたくなったときぐらいしか浮かびません。 Rails が提供する MVC に乗かりたいなら AbstroctController を使うほうが楽そうです。

補足としたほうを読んでみると Rails の仕組みも見えてくるような気がしますね。(#知らんけど)

補足のまとめ

登場人物を整理しておきます。

  • view_context - ActionView::Base のサブクラスのインスタンス。ビューの中のself
  • lookup_context - ActionView::LookupContext のインスタンス。テンプレートを探してくれる。テンプレートを探すための情報ももってる。
  • renderer - render を実際に行うところ。今回は登場してない。render の引数によってどのクラスを使うか選択される。

コントローラーと連携するためにコントローラに機能を追加する人たちとして、

  • ActionView::Layouts
  • ActionView::Rendering
  • ActionView::ViewPaths

が登場しました。

関連