sinatra/sinatra-contrib/lib/sinatra/content_for.rb

198 lines
5.9 KiB
Ruby

# frozen_string_literal: true
require 'sinatra/base'
require 'sinatra/capture'
module Sinatra
# = Sinatra::ContentFor
#
# <tt>Sinatra::ContentFor</tt> is a set of helpers that allows you to capture
# blocks inside views to be rendered later during the request. The most
# common use is to populate different parts of your layout from your view.
#
# The currently supported engines are: Erb, Erubi, Haml and Slim.
#
# == Usage
#
# You call +content_for+, generally from a view, to capture a block of markup
# giving it an identifier:
#
# # index.erb
# <% content_for :some_key do %>
# <chunk of="html">...</chunk>
# <% end %>
#
# Then, you call +yield_content+ with that identifier, generally from a
# layout, to render the captured block:
#
# # layout.erb
# <%= yield_content :some_key %>
#
# If you have provided +yield_content+ with a block and no content for the
# specified key is found, it will render the results of the block provided
# to yield_content.
#
# # layout.erb
# <% yield_content :some_key_with_no_content do %>
# <chunk of="default html">...</chunk>
# <% end %>
#
# === Classic Application
#
# To use the helpers in a classic application all you need to do is require
# them:
#
# require "sinatra"
# require "sinatra/content_for"
#
# # Your classic application code goes here...
#
# === Modular Application
#
# To use the helpers in a modular application you need to require them, and
# then, tell the application you will use them:
#
# require "sinatra/base"
# require "sinatra/content_for"
#
# class MyApp < Sinatra::Base
# helpers Sinatra::ContentFor
#
# # The rest of your modular application code goes here...
# end
#
# == And How Is This Useful?
#
# For example, some of your views might need a few javascript tags and
# stylesheets, but you don't want to force this files in all your pages.
# Then you can put <tt><%= yield_content :scripts_and_styles %></tt> on your
# layout, inside the <head> tag, and each view can call <tt>content_for</tt>
# setting the appropriate set of tags that should be added to the layout.
#
# == Limitations
#
# Due to the rendering process limitation using <tt><%= yield_content %></tt>
# from within nested templates do not work above the <tt><%= yield %> statement.
# For more details https://github.com/sinatra/sinatra-contrib/issues/140#issuecomment-48831668
#
# # app.rb
# get '/' do
# erb :body, :layout => :layout do
# erb :foobar
# end
# end
#
# # foobar.erb
# <% content_for :one do %>
# <script>
# alert('one');
# </script>
# <% end %>
# <% content_for :two do %>
# <script>
# alert('two');
# </script>
# <% end %>
#
# Using <tt><%= yield_content %></tt> before <tt><%= yield %></tt> will cause only the second
# alert to display:
#
# # body.erb
# # Display only second alert
# <%= yield_content :one %>
# <%= yield %>
# <%= yield_content :two %>
#
# # body.erb
# # Display both alerts
# <%= yield %>
# <%= yield_content :one %>
# <%= yield_content :two %>
#
module ContentFor
include Capture
# Capture a block of content to be rendered later. For example:
#
# <% content_for :head do %>
# <script type="text/javascript" src="/foo.js"></script>
# <% end %>
#
# You can also pass an immediate value instead of a block:
#
# <% content_for :title, "foo" %>
#
# You can call +content_for+ multiple times with the same key
# (in the example +:head+), and when you render the blocks for
# that key all of them will be rendered, in the same order you
# captured them.
#
# Your blocks can also receive values, which are passed to them
# by <tt>yield_content</tt>
def content_for(key, value = nil, options = {}, &block)
block ||= proc { |*| value }
clear_content_for(key) if options[:flush]
content_blocks[key.to_sym] << capture_later(&block)
end
# Check if a block of content with the given key was defined. For
# example:
#
# <% content_for :head do %>
# <script type="text/javascript" src="/foo.js"></script>
# <% end %>
#
# <% if content_for? :head %>
# <span>content "head" was defined.</span>
# <% end %>
def content_for?(key)
content_blocks[key.to_sym].any?
end
# Unset a named block of content. For example:
#
# <% clear_content_for :head %>
def clear_content_for(key)
content_blocks.delete(key.to_sym) if content_for?(key)
end
# Render the captured blocks for a given key. For example:
#
# <head>
# <title>Example</title>
# <%= yield_content :head %>
# </head>
#
# Would render everything you declared with <tt>content_for
# :head</tt> before closing the <tt><head></tt> tag.
#
# You can also pass values to the content blocks by passing them
# as arguments after the key:
#
# <%= yield_content :head, 1, 2 %>
#
# Would pass <tt>1</tt> and <tt>2</tt> to all the blocks registered
# for <tt>:head</tt>.
def yield_content(key, *args, &block)
if block_given? && !content_for?(key)
haml? && Tilt[:haml] == Tilt::HamlTemplate && respond_to?(:capture_haml) ? capture_haml(*args, &block) : yield(*args)
else
content = content_blocks[key.to_sym].map { |b| capture(*args, &b) }
content.join.tap do |c|
if block_given? && (erb? || erubi?)
@_out_buf << c
end
end
end
end
private
def content_blocks
@content_blocks ||= Hash.new { |h, k| h[k] = [] }
end
end
helpers ContentFor
end