Edited at
SpeeeDay 18

Rails Viewでテンプレート継承する話

More than 3 years have passed since last update.

この記事は Speee Advent Calendar の18日目です。


テンプレート継承とは

ページが表示する内容をブロック単位で定義して、ベースとなるテンプレートに その内容を継承する View の構築方法です。

言葉では全然頭に入ってこないので、以下の例をどうぞ。


Jade (Node) の例


テンプレート


top.jade

extends ./base.jade

block title
title タイトル

block content
h1 Hello, world!
hr
p This is template inheritance.



base.jade

doctype html

html
head
block title
title untitled
body
block content


Twig (PHP) の例


テンプレート


top.html

{% extends "base.html" %}

{% block title %}タイトル{% endblock %}

{% block content %}
<h1>Hello, world!</h1>
<hr>
<p>This is template inheritance.</p>
{% endblock %}



base.html

<!DOCTYPE html>

<html>
<head>
<link rel="stylesheet" href="style.css" />
<title>{% block title %}untitled{% endblock %} - テンプレート継承の話</title>
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
</body>
</html>


出力 (整形したもの)

<!DOCTYPE html>

<html>
<head>
<link rel="stylesheet" href="style.css" />
<!-- title content -->
<title>タイトル - テンプレート継承の話</title>
</head>
<body>
<div id="content">
<!-- block content -->
<h1>Hello, world!</h1>
<hr>
<p>This is template inheritance.</p>
<!-- /block content -->
</div>
</body>
</html>



  1. top.html に定義された block 内でテンプレートの内容を定義する。


  2. extend で指定されたベーステンプレートに継承する。

  3. ベーステンプレートで同名の block が呼び出され、継承元で定義された値が使われるようになる。


    • 継承元で定義されなかった場合は、ベーステンプレートの定義が使われる (例: title なら "untitled")



例に挙げた Jade や Twig など、割とモダンなテンプレートエンジンには実装されている傾向です。

あまり注目されませんが、実は Smarty なんかにも同様の実装 があります。


Rails でテンプレ継承 → content_for と友達になる

こういう書き方を Rails でやりたい時ってどうするんだ、って話ですが、content_for 系のメソッドを上手に活用することで実現できます(ActionView::Helpers::CaptureHelper にて定義されています)。

実はちゃんと Rails ガイドの最後の方で、その方法が言及されている のですが。

Slim で要点のみ整理すると、以下みたいな感じになります。


layouts/hello.slim

- content_for :stylesheets

| #hello {
display: none;
}

- content_for :content
h2 Hello content_for!
p This is template inheritance.

= render template: 'layouts/application'



layouts/application.slim

doctype html

html
head
title Page title
style
= yield :stylesheets
body
#hello
h1 hello
hr
= content_for?(:content) ? yield(:content) : yield

スクリーンショット 2015-12-18 15.40.37.png

表示としては、hello.slim で定義した CSS で div#hello が隠された上で、

content として指定したブロックの内容が表示されるようになります。


content_for :hoge <=> yield :hoge

content_for が既存テンプレートエンジンの block に相当する部分です。

Ruby block を渡すことで、ブロック内の出力に名前が付き、レンダリング終了まで保持されます。

定義されたブロックの内容を出力する場合は、 yield の引数に名前を指定することで出力できます。


content_for?

= content_for?(:content) ? yield(:content) : yield

ぱっと見ると、この行だけ謎めいていますが、大したことではなくて、:content という名前のブロックが定義されていれば、その内容を呼び出すというだけです。

もし無い場合は、普通の Rails layout と同じように、メインの出力内容を呼び出す 動きになります。


デフォルト値を使う

content_for? による判定を使えば、Jade や Twig のように、レイアウト側でデフォルト値を定義する事も可能です。

= content_for?(:content) ? yield(:content) : 'Default content'


応用: content_for で複数回定義


複数回のcontent_for

- content_for :metas

= csrf_meta_tags

- content_for :metas
= tag :meta, name: :robots, content: :noindex

- content_for :metas
= tag :meta, name: 'twitter:card', content: :summary_large_image
= tag :meta, name: 'twitter:domain', content: '...'


content_for は、同じ名前に対して複数回定義ができ、呼び出すたびに ブロックの後ろに内容が継ぎ足されて行きます

yield を行ったタイミングで全部一気に表示されるので、meta タグなんかで活用すると良さそうですね。参考...

もし内容を上書きしたい場合は、flush: true を指定しましょう。


上書き

- content_for :metas, flush: true

/! 無


まとめ

content_for は良いヤツです。 仲間にして損は無し。

実際に テンプレート継承にするか、それとも partial render にするかは、どのようなページを構築するかに依るかと思いますので、適材適所で使いどころを見極めましょう。



おまけ: gem化

既存テンプレートエンジンのような機能(デフォルト値、append/prepend など)を扱うためには、View 側でやや実装が必要になるので、もっと手軽にできても良さそうです。

そんなこんなで gem の勉強も兼ね、Rails View にテンプレート継承機能を追加する Jubako gem を作ってみました。Layout とあわせて、テンプレート継承、多段継承を扱えます。

まだ生まれたてで、 テストすら無い ので、実践投入は控えていただきたいですが、検証等は歓迎です。

yhatt/jubako - Github

実装は Jade を意識しており、block append: :... block prepend: :... 等もあります。Slim と組み合わせる とそれっぽい感じ。

 

ちなみに仮名称は、Jade に語感を合わせた "Jedi" (ジェダイ) でした。 エピソード7は本日公開です。