railsのViewをDRYにするために行ってること


## simple_form

フォーム作成に手間が少なくなるsimple_formを使っている人は多いと思います。
まずsimple_formを使ってViewをDRYにする方法です。

### 準備

#### インストール

```ruby:Gemfile
gem 'simple_form' # 必須
gem 'twitter-bootstrap-rails', :git => 'git://github.com/seyhunak/twitter-bootstrap-rails.git', :branch => "bootstrap3" # TwitterBootstrap本体
gem 'less-rails' # TwitterBootstrapを使うのに必要
gem 'therubyracer' # TwitterBootstrapを使うのに必要
gem 'slim-rails' # slimを使いたい
```

```bash:bash
# プロジェクトのルートに移動
$ cd project_home
# gemインストール
$ bundle install
# TwitterBootstrapインストール
$ rails g bootstrap:install less
# レイアウトを作成
$ rails g bootstrap:layout application fluid
# simple_formインストール
$ rails generate simple_form:install --bootstrap
# サンプルのBookモデルを作成
$ rails g scaffold Book title:string author:string review:integer description:text
# DBを作成
$ rake db:migrate
# 作成されたBook関連のviewにTwitterBootstrapを適用する
$ rails g bootstrap:themed Books
```

#### 日本語化とテキストの共通化

まずは日本語化

```ruby:config/application.rb
config.i18n.default_locale = :ja # 追加
```

##### 日本語化ファイルを追加
1. config/locales/ja.yml  
https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml
2. config/locales/ja.bootstrap.yml  
https://github.com/heartrails/rails4-example/blob/master/config/locales/ja.bootstrap.yml
3. config/locales/simple_form.ja.yml
https://github.com/MiraitSystems/enju_trunk/blob/master/config/locales/simple_form.ja.yml

日本語化するときにこちらも役立つ
http://qiita.com/yujiym/items/b32c1a788aaa34eb8089
http://qiita.com/yshr04hrk/items/cb7b81c104321f660661
http://morizyun.github.io/blog/i18n-english-rails-ruby-many-languages/

##### 共通化

モデルのカラム名やモデル名を共通化を行うのに、locales.ymlを使用する。
ただ、作成したBookの一覧と追加画面を見てみると名称が統一されていない。

日本語化を行うと以下のように修正画面のラベルは日本語化されている。
![スクリーンショット 2015-07-19 16.49.04.png](https://qiita-image-store.s3.amazonaws.com/0/35451/13f5e880-6486-0f14-e7d2-22bc83449e5e.png "スクリーンショット 2015-07-19 16.49.04.png")

一覧画面を見てみると日本語化したはずのカラム名が英語で表示されている。
![スクリーンショット 2015-07-19 16.44.02.png](https://qiita-image-store.s3.amazonaws.com/0/35451/276bad34-cc78-0fc6-0077-0ce1b23ceb83.png "スクリーンショット 2015-07-19 16.44.02.png")

ここでindex.html.slimのソースを見てみる。

```ruby:views/books/index.html.slim
table class="table table-striped"
  thead
    tr
      th=model_class.human_attribute_name(:id)
      th=model_class.human_attribute_name(:title)
      th=model_class.human_attribute_name(:author)
      th=model_class.human_attribute_name(:review)
      th=model_class.human_attribute_name(:desc)
      th=model_class.human_attribute_name(:created_at)
      th=t '.actions', :default => t("helpers.actions")
```
フォームのラベル部分がきちっと日本語になっているのはconfig/locales/simple_form.ja.ymlの
labels: => book: => id: の値を参照しているからで、一覧はこの値を見ていないため英語で表示されてしまう。
そのため、一覧のタイトルもここの値を見るように下記のように変更する。

```ruby:views/books/index.html.slim
table class="table table-striped"
  thead
    tr
      th=t 'simple_form.labels.book.id'
      th=t 'simple_form.labels.book.title'
      th=t 'simple_form.labels.book.author'
      th=t 'simple_form.labels.book.review'
      th=t 'simple_form.labels.book.desc'
      th=t 'simple_form.labels.defaults.created_at'
```
tメソッドの説明は下記の通り。
http://morizyun.github.io/blog/i18n-english-rails-ruby-many-languages

### カスタムインプット
バリデーションのためのHTMLのオプションや、入力のためのヒントメッセージ、プレースホルダを毎度viewに記述するのはDRYの流儀に反する。
そんなときに使用するのがカスタムインプット

#### モデルに指定している入力制限(1~5の範囲の値)のみを許可する入力項目をカスタムインプットで作成する
1. モデルにバリデーションを作成

```ruby:models/book.rb
class Book < ActiveRecord::Base
  validates :review, numericality: {
                      only_integer: true,
                      greater_than_or_equal_to: 1,
                      less_than_or_equal_to: 3 }
end
```
2. app/inputsディレクトリを作成
3. inputs/review_input.rbを作成

```ruby:inputs/review_input.rb
class ReviewInput < SimpleForm::Inputs::NumericInput
  def input
    # クラスを追加
    input_html_classes.push('review form-control')
    # inputタグのオプションにminとmaxが設定される
    min, max = %i(min max).collect {|sym| input_html_options[sym] }
    # テキストボックスの下に入力のヒントが表示される
    options.merge!(hint: "※#{min}~#{max}の数値を入力")
    super
  end
end
```	

```ruby:views/books/_form.html.slim
= simple_form_for @book, :html => { :class => "form-horizontal" } do |f|
  = f.input :title
  = f.input :author
  \ as: :カスタムインプット名
  = f.input :review, as: :review
  = f.input :desc
  = f.button :submit, :class => 'btn-primary'
  = link_to t('.cancel', :default => t("helpers.links.cancel")), books_path, :class => 'btn btn-default'
```

実行してみると以下のようになる

![スクリーンショット 2015-07-19 18.31.40.png](https://qiita-image-store.s3.amazonaws.com/0/35451/2a387da6-adae-5498-373a-6d0c018c18ce.png "スクリーンショット 2015-07-19 18.31.40.png")

これでもしモデルのバリデーションのmin maxが変更されたときにview側も変更せずに済む。

カスタムインプットの作り方は以下が参考になる
http://www.ohmyenter.com/?p=214
https://github.com/plataformatec/simple_form/wiki/Custom-inputs-examples

同様にフォーム単位で作成できるカスタムフォームがある
https://github.com/plataformatec/simple_form

## yieldとcontent_for
TwitterBootstrapをインストールしてレイアウトを作成するとlayouts/application.html.slimは以下のようになっている

```ruby:layouts/application.html.slim
doctype html
html lang="en"
  head
    # 省略
  body
    # ヘッダー
    .navbar.navbar-default.navbar-static-top
        .container
          button.navbar-toggle type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse"
            span.icon-bar
            span.icon-bar
            span.icon-bar
          a.navbar-brand href="#"Book
          .navbar-collapse.collapse.navbar-responsive-collapse
            ul.nav.navbar-nav
              li= link_to "Link 1", "/path1"
              li= link_to "Link 2", "/path2"
              li= link_to "Link 3", "/path3"

    .container
      .row
        .col-lg-9
          = bootstrap_flash
          # コンテンツ出力
          = yield
        # サイドバー
        .col-lg-3
          .well.sidebar-nav
            h3 Sidebar
            ul.nav.nav-list
              li.nav-header Sidebar
              li= link_to "Link 1", "/path1"
              li= link_to "Link 2", "/path2"
              li= link_to "Link 3", "/path3"
      # フッター
      footer
        p &copy; Company 2015
```

すべての画面で同様のレイアウトでヘッダー サイドバー フッターを表示するのであればこれでも問題ないが、画面によってヘッダーのデザインを変えたりサイドバーを表示しなかったりする場合これでは問題がある。
そんなときにyieldとcontent_forを使うとレイアウトの出しわけを容易に行うことができる。

### yield
ブロックつきメソッドでブロックを呼び出すときに使用する。
viewで使用するときはcontent_forで定義されたブロックを呼び出すときに使用するくらいに認識しておけば今回は大丈夫かと思います。

### content_for
ブロックをレイアウトファイルに渡すことができる。

```ruby:content_for
# ブロックの作成
content_for :ラベル名 do
  # 行いたい処理
end

# ブロックの呼び出し
yield :ラベル名
```

### yieldとcontent_forを使用してレイアウトを作成する

```ruby:views/layouts/application.html.slim
- content_for :head do
  = stylesheet_link_tag "application", :media => "all", "data-turbolinks-track" => true
  = javascript_include_tag "application", "data-turbolinks-track" => true
- content_for :favicon, favicon_link_tag

= render :partial => "layouts/base"
```

```ruby:views/layouts/_base.html.slim
doctype html
html lang="ja"
  head
    meta charset="utf-8"
    meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"
    meta name="viewport" content="width=device-width, initial-scale=1.0"
    title= content_for?(:title) ? yield(:title) : "SimpleForm"
    = csrf_meta_tags
    = yield :head
    = favicon_link_tag 'favicon.ico', :rel => 'shortcut icon'

  body
    = yield :header
    = render('layouts/content')
    = yield :footer
```

```ruby:views/layouts/_header.html.slim
.navbar.navbar-default.navbar-static-top
    .container
      button.navbar-toggle type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse"
        span.icon-bar
        span.icon-bar
        span.icon-bar
      a.navbar-brand href="#"SimpleForm
      .navbar-collapse.collapse.navbar-responsive-collapse
        ul.nav.navbar-nav
          li= link_to "Link 1", "/path1"
          li= link_to "Link 2", "/path2"
          li= link_to "Link 3", "/path3"
```

```ruby:views/layouts/_content.html.slim
.container
  .row
    .col-lg-9
      = bootstrap_flash
      \ このyieldでレイアウトファイルが呼び出される
      = yield
    = yield :sidebar
```

```ruby:views/layouts/_sidebar.html.slim
.col-lg-3
  .well.sidebar-nav
    h3 Sidebar
    ul.nav.nav-list
      li.nav-header Sidebar
      li= link_to "Link 1", "/path1"
      li= link_to "Link 2", "/path2"
      li= link_to "Link 3", "/path3"
```

```ruby:views/layouts/_footer.html.slim
.container
  footer
    p &copy; Company 2015
```

_base.html.slimで必要なlayoutsの呼び出しをyieldで行っている。
次にテンプレートに対応したブロックをview側でcontent_for使用してブロックを作成する。

```ruby:views/books/index.html.slim
# ブロックの作成
# ヘッダー
- content_for :header, render('layouts/header')
# サイドバー
- content_for :sidebar, render('layouts/sidebar')
# フッター
- content_for :footer, render('layouts/footer')

- model_class = Book
div class="page-header"
  h1=t '.title', :default => model_class.model_name.human.pluralize.titleize
table class="table table-striped"
# 省略
```

ヘッダー サイドバー フッターをいずれか表示しない場合はcontent_forを呼び出さない。
yieldでは呼び出そうとしているブロックが存在しなくてもエラーにはならないので問題ない。
内容を変更する場合はcontent_forの第二引数に指定したrenderの値を変更する。

そのほかの使い方としてはGoogleにインデックスされたくないページにnoindexを指定する場合です。
Googleのクローラにインデックスさせないことを知らせるためにnoindexメタタグをヘッダに指定する必要がある。
ページによって出力するしないを設定するにはyieldとcontent_forを使用するのが最適です。

```ruby:views/layouts/_base.html.slim
doctype html
html lang="ja"
  head
    meta charset="utf-8"
    meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"
    meta name="viewport" content="width=device-width, initial-scale=1.0"
    title= content_for?(:title) ? yield(:title) : "SimpleForm"
    = csrf_meta_tags
    = yield :head
    = yield :noindex
    = favicon_link_tag 'favicon.ico', :rel => 'shortcut icon'

  body
    = yield :header
    = render('layouts/content')
    = yield :footer
```

```ruby:_noindex.html.slim
meta content="noindex" name="robots"
```

```ruby:views/books/index.html.slim
# インデックスされたくないテンプレートビュー
- content_for :header, render('layouts/noindex')

- model_class = Book
div class="page-header"
  h1=t '.title', :default => model_class.model_name.human.pluralize.titleize
table class="table table-striped"
# 省略
```