この記事は Speee Advent Calendar の18日目です。
テンプレート継承とは
ページが表示する内容をブロック単位で定義して、ベースとなるテンプレートに その内容を継承する View の構築方法です。
言葉では全然頭に入ってこないので、以下の例をどうぞ。
Jade (Node) の例
テンプレート
extends ./base.jade
block title
title タイトル
block content
h1 Hello, world!
hr
p This is template inheritance.
doctype html
html
head
block title
title untitled
body
block content
Twig (PHP) の例
テンプレート
{% extends "base.html" %}
{% block title %}タイトル{% endblock %}
{% block content %}
<h1>Hello, world!</h1>
<hr>
<p>This is template inheritance.</p>
{% endblock %}
<!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>
-
top.html
に定義されたblock
内でテンプレートの内容を定義する。 -
extend
で指定されたベーステンプレートに継承する。 - ベーステンプレートで同名の
block
が呼び出され、継承元で定義された値が使われるようになる。- 継承元で定義されなかった場合は、ベーステンプレートの定義が使われる (例:
title
なら "untitled")
- 継承元で定義されなかった場合は、ベーステンプレートの定義が使われる (例:
例に挙げた Jade や Twig など、割とモダンなテンプレートエンジンには実装されている傾向です。
あまり注目されませんが、実は Smarty なんかにも同様の実装 があります。
Rails でテンプレ継承 → content_for と友達になる
こういう書き方を Rails でやりたい時ってどうするんだ、って話ですが、content_for
系のメソッドを上手に活用することで実現できます(ActionView::Helpers::CaptureHelper にて定義されています)。
実はちゃんと Rails ガイドの最後の方で、その方法が言及されている のですが。
Slim で要点のみ整理すると、以下みたいな感じになります。
- content_for :stylesheets
| #hello {
display: none;
}
- content_for :content
h2 Hello content_for!
p This is template inheritance.
= render template: 'layouts/application'
doctype html
html
head
title Page title
style
= yield :stylesheets
body
#hello
h1 hello
hr
= content_for?(:content) ? yield(:content) : yield
表示としては、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 :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 とあわせて、テンプレート継承、多段継承を扱えます。
まだ生まれたてで、 テストすら無い ので、実践投入は控えていただきたいですが、検証等は歓迎です。
実装は Jade を意識しており、block append: :...
block prepend: :...
等もあります。Slim と組み合わせる とそれっぽい感じ。
ちなみに仮名称は、Jade に語感を合わせた "Jedi" (ジェダイ) でした。 エピソード7は本日公開です。