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


この記事は [Speee Advent Calendar](http://qiita.com/advent-calendar/2015/speee) の18日目です。

## テンプレート継承とは

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

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

### [Jade](http://jade-lang.com/reference/extends/) (Node) の例

#### テンプレート

```jade:top.jade
extends ./base.jade

block title
  title タイトル

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

```jade:base.jade
doctype html
html
  head
    block title
      title untitled
  body
    block content
```

### [Twig](http://twig.sensiolabs.org/doc/tags/extends.html) (PHP) の例

#### テンプレート

```html:top.html
{% extends "base.html" %}

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

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

```html: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>
```

### 出力 (整形したもの)

```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 なんかにも同様の実装](http://www.smarty.net/docs/ja/language.function.extends.tpl) があります。

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

こういう書き方を Rails でやりたい時ってどうするんだ、って話ですが、`content_for` 系のメソッドを上手に活用することで実現できます（[ActionView::Helpers::CaptureHelper](http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html) にて定義されています）。

実はちゃんと [Rails ガイドの最後の方で、その方法が言及されている](http://railsguides.jp/layouts_and_rendering.html#%E3%83%8D%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B) のですが。
Slim で要点のみ整理すると、以下みたいな感じになります。

```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'
```

```slim: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](https://qiita-image-store.s3.amazonaws.com/0/70467/618d993d-f9e9-bab8-1d11-2eb5ffad2b5e.png "スクリーンショット 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?`

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

ぱっと見ると、この行だけ謎めいていますが、大したことではなくて、`:content` という名前のブロックが定義されていれば、その内容を呼び出すというだけです。
もし無い場合は、[普通の Rails layout と同じように、メインの出力内容を呼び出す](http://railsguides.jp/layouts_and_rendering.html#yield%E3%82%92%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B) 動きになります。

#### デフォルト値を使う

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

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

### 応用: `content_for` で複数回定義

```slim:複数回の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` タグなんかで活用すると良さそうですね。[参考...](http://whitech0c0late.hatenablog.com/entry/2013/12/26/232649)

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

```slim:上書き
- content_for :metas, flush: true
  /! 無
```

## まとめ

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

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

---

#### おまけ: gem化

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

そんなこんなで gem の勉強も兼ね、Rails View にテンプレート継承機能を追加する **[Jubako](https://github.com/yhatt/jubako)** gem を作ってみました。Layout とあわせて、テンプレート継承、多段継承を扱えます。
まだ生まれたてで、 ***テストすら無い*** ので、実践投入は控えていただきたいですが、検証等は歓迎です。

**[yhatt/jubako - Github](https://github.com/yhatt/jubako)**

実装は Jade を意識しており、`block append: :...` `block prepend: :...` 等もあります。[Slim と組み合わせる](https://github.com/yhatt/jubako#layout-template-inheritance) とそれっぽい感じ。

　

ちなみに仮名称は、Jade に語感を合わせた **"Jedi"** (ジェダイ) でした。 **[エピソード7は本日公開です。](http://starwars.disney.co.jp/home.html)**