Sinatra で content_for を使う.

最近、Sinatraを使い出しました. 凄く手軽です.

シンプルだけあってRailsにある機能が欲しいときが良くあります.

Railsのcontent_for

知らない人はググってください. でもまー、概要ぐらいは説明します.

RailsSinatraもテンプレートを外枠と内枠で分けることができます. でも内枠から外枠に値を渡したいときがあります。そんなときに役立つのがcontent_forです.

(例)タイトルを変えてみる

外枠のテンプレート「layout.rb」

<html>
<head><title><%= yield :title %></title></head>
<body>
<%= yield %>
</body>
</html>


内枠のテンプレート「index.rb」

<% content_for :title, 'よほほほ' %>
ブルックのステッカー買ったけど張る場所がない.


実行結果

<html>
<head><title>よほほほ</title></head>
<body>
ブルックのステッカー買ったけど張る場所がない.
</body>
</html>

こんな感じの機能です.

Sinatra での実現方法

では本題、Sinatraではcontent_forがありません.

手軽な方法としてはsinatraプラグインsinatra-content-forがあります.

でも今回はプラグインを使わずにやってみました.

以下のコードをapp.rbとかsinatraの実装ファイルから参照するなり、貼付けてください. それだけでcontent_forが使えます.

module Sinatra
  module Helpers
    def content_for(name, content=nil, &block)
      @content_for_param ||= {}
      content ||= block
      if content.nil?
        if @content_for_param.include?(name)
          @content_for_param[name].join
        else
          nil
        end
      else
        content = content.call if content.is_a? Proc
        content = content.dup if content.is_a? String
        @content_for_param[name] ||= []
        @content_for_param[name] << content
        nil
      end
    end
  end
  
  module Templates
    def render_with_content_for(engine, data, options={}, locals={}, &block)
      if block_given?
        interrupt_block = Proc.new { |name|
          if name.nil?
            block.call
          else
            content_for name
          end
        }
        render_without_content_for engine, data, options, locals, &interrupt_block 
      else
        render_without_content_for engine, data, options, locals
      end
    end
    alias_method :render_without_content_for, :render
    alias_method :render, :render_with_content_for
  end
end

っで使い方ですがRailsのcontent_forと全く同じってわけではないです. blockを使った表記がerbのプリコンパイルが期待通りに成ってなかったのでちょっと工夫する必要があります.

本来なら以下のように書けると思ったのですが

<% content_for(:title) do %>
FooBar
<% end %>

プリコンパイルを見てみると以下のような感じで内枠の文字列として追加されちゃってますorz

content_for(:title) do ;
@_out_buf.concat "FooBar";
end;

そこで工夫してあげると.以下の書き方で回避できました.

<% content_for(:title){
  "FooBar"
} %>

ちなみに以下のようにも書けます.

<% content_for(:title, "FooBar") %>

さらに同じ名前で数回コールすると結合されるようにしました。

<% content_for(:title, "FooBar") %>
<% content_for(:title, "Boo") %>
=> FooBarBoo