Features
VERSION 4.0.0
The blocks gem is many things.
It acts as:
- a container for reusable blocks of code and options
- a common interface for rendering code, whether the code was defined previously in Ruby blocks, Ruby Methods, Rails partials, or proxies to other blocks of code
- a series of hooks and wrappers that can be utilized to render code before, after, and around other blocks of code, as well as before each, after each, and around each item in a collection
- a templating utility for easily building reusable and highly customizable UI components
- a means for DRYing up oft-repeated code in your layouts and views
- a simple mechanism for changing or skipping the rendering behavior for particular blocks of code
Essentially, this all boils down to the following: Blocks makes it easy to define blocks of code that can be rendered either verbatim or with replacements and modifications at some later point in time.
Installation & Prerequisites
Blocks requires Rails 3.0 or greater and Ruby 2.0 or greater.
It has been tested with Ruby 2.0.0 through 2.5.1 (as well as ruby-head), and Rails 3.0 - 5.2 (as well as Edge Rails)
gem 'blocks'
bundle install
if !Object.respond_to?(:yaml_as)
class Object
def self.yaml_as(*args)
yaml_tag(*args)
end
end
end
Overview
There are many ways to render content to the screen in web applications. Ruby on Rails offers a couple of default strategies right off the bat through its combination of ActionController, ActionView, and ERB templating language:
- Rendering content within an ActionView template (the controller action) and layout
- Pulling in additional content through rendering partials
- Capturing content from a block and outputting that content at some later point
- Calling a different method from the controller action which determines what and how to render
There are obviously additional means to render output, such as the redirect methods in a controller and rendering inline code, but generally speaking, most generated output come about through one of those above methods.
The approaches listed above can essentially be generalized as follows
- Rendering using a method
- Rendering fragments (using a Rails’ partial)
- Storing content for later rendering (using a Ruby Block)
- Proxying to another source that knows what to / how to render
The Blocks gem attempts to take these four concepts and provide one common interface.
The reasoning behind this is simple.
Defining Blocks
With Blocks, you can define a block of code for later rendering using Ruby blocks, Rails partials, and proxies to other blocks or methods.
A block consists of a name, an optional hash of options, and a rendering strategy (also called its definition).
<% blocks.define :my_block %>
<!-- SAME AS -->
<% blocks.define "my_block" %>
- blocks.define :my_block
#- SAME AS
- blocks.define "my_block"
# where builder is an instance
# of Blocks::Builder
builder.define :my_block
# SAME AS
builder.define "my_block"
A block’s name can be a symbol or a string. The underlying system treats symbols and strings the same. Therefore, any block that is defined with a String name can be accessed with its corresponding symbol name and vice-versa.
With a Ruby Block
<% blocks.define :my_block do %>
content
<% end %>
- blocks.define :my_block do
content
# where builder is an instance
# of Blocks::Builder
builder.define :my_block do
"content"
end
A Block may be defined as a standard Ruby block.
With a Proc
<% my_block =
Proc.new { "content" } %>
<% blocks.define :my_block,
block: my_block %>
<!-- OR -->
<% blocks.define :my_block,
&my_block %>
- my_block = Proc.new { "content" }
- blocks.define :my_block,
block: my_block
-# OR
- blocks.define :my_block, &my_block
# where builder is an instance
# of Blocks::Builder
my_block = Proc.new { "content" }
builder.define :my_block,
block: my_block
#OR...
builder.define :my_block, &my_block
It may also be defined with a Proc
With a Lambda
<% my_block = lambda { "content" } %>
<% blocks.define :my_block,
block: my_block %>
<!-- OR -->
<% blocks.define :my_block, &my_block %>
Lambdas are kind of a pain in Haml
and Procs should be used instead
# where builder is an instance
# of Blocks::Builder
my_block = lambda { "content" }
builder.define :my_block,
block: my_block
# OR...
builder.define :my_block, &my_block
It may also be defined with a Lambda
With a Rails Partial
<%= blocks.define :my_block,
partial: "my_partial" %>
- blocks.define :my_block,
partial: "my_partial"
# where builder is an instance
# of Blocks::Builder
- builder.define :my_block,
partial: "my_partial"
A Block may be defined as a Rails partial using the “partial” keyword in the parameters. Whenever the Block gets rendered, the partial actually gets rendered.
With a Proxy to Another Block
<% blocks.define :my_block,
with: :some_proxy_block %>
- blocks.define :my_block,
with: :some_proxy_block
# where builder is an instance
# of Blocks::Builder
- builder.define :my_block,
with: :some_proxy_block
A Block may be defined as a proxy to another block using the “with” keyword in the parameters.
Proxying to a method
<% builder.define :my_block,
with: :column %>
- builder.define :my_block,
with: :column
builder.define :my_block,
with: :column
Where “builder” is an instance of a class that extends Blocks::Builder and has a method called “column”.
The “with” keyword may also specify the name of a method that will be called on the builder instance when the block is rendered. By default, this will be an instance of Blocks::Builder.
Also, a Proxy block can point to a method on the builder instance (by default, this is an instance of Blocks::Builder).
Proxying to a Proxy
<% blocks.define :my_block,
with: :proxy_1 %>
<% blocks.define :proxy_1,
with: :proxy_2 %>
<% blocks.define :proxy_2 do %>
My proxied proxied content
<% end %>
- blocks.define :my_block,
with: :proxy_1
- blocks.define :proxy_1,
with: :proxy_2
- blocks.define :proxy_2 do
My proxied proxied content
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
with: :proxy_1
builder.define :proxy_1,
with: :proxy_2
builder.define :proxy_2 do
"My proxied proxied content"
end
Proxy Blocks can also be chained together though separate definitions. The order of Block definitions is irrelevant - with two caveats:
- Proxies must be setup before attempting to render them
- If the same block name is defined multiple times with different proxy names, the first one defined will be used
With Multiple Definitions
<% blocks.define :my_block,
partial: "my_partial" %>
<% blocks.define :my_block,
with: :my_proxy %>
<% blocks.define :my_block do %>
My Block Definition
<% end %>
- blocks.define :my_block,
partial: "my_partial"
- blocks.define :my_block,
with: :my_proxy
- blocks.define :my_block do
My Block Definition
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
partial: "my_partial"
builder.define :my_block,
with: :my_proxy
builder.define :my_block do
"My Block Definition"
end
:my_block will use the partial: “my_partial” when rendered
When multiple definitions for the same block name are provided, Blocks will utilize the first definition to occur, whether it’s a Ruby block, a Rails partial, or a Proxy to another block.
Without a Definition
<% blocks.define :my_block %>
- blocks.define :my_block
# where builder is an instance
# of Blocks::Builder
builder.define :my_block
Blocks do not need to have a definition provided. When they are rendered, no output is generated.
This, in itself, is not particularly useful, but can become more and more useful when block options and hooks and wrappers are combined.
With a Collection
<% blocks.define :my_block,
collection: [1, 2, 3, 4] do |item| %>
<li>Item: <%= item %></li>
<% end %>
- blocks.define :my_block,
collection: [1, 2, 3, 4] do |item|
%li= "Item: #{item}"
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
collection: [1, 2, 3, 4] do |item|
builder.content_tag :li,
"Item #{item}"
end
A collection may be defined for a block, in which case, when that block is rendered, it will actually render the block multiple times, once for each item in the collection.
With an Alias
<% blocks.define :my_block,
collection: [1, 2, 3, 4],
as: :item,
partial: "my_partial" %>
- blocks.define :my_block,
collection: [1, 2, 3, 4],
as: :item,
partial: "my_partial"
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
collection: [1, 2, 3, 4],
as: :item,
partial: "my_partial"
Additionally, you can set an alias for each item in the collection as the collection is iterated over. This is done using the “as” option.
If the block being rendered is a partial, it will alias each item in the collection with the specified option (i.e. the value of the “as” option will become the name of the variable available in the partial being rendered and will contain each item in the collection being rendered). Additionally, “current_index” will also be a variable that can be accessed within the partial, and will correspond to the item’s index in the collection.
If the block being rendered is not a partial, it will store the alias name as a key in an options hash that will be optionally passed to the block when it is rendered. Additionally, “current_index” will store the item’s current index within the collection.
Without an Alias
When no alias is specified and the block being rendered is a partial, it will alias each item in the collection as “object” (i.e. “object” will become the name of the variable available in the partial being rendered and will contain each item in the collection being rendered). Additionally, “current_index” will also be a variable that can be accessed within the partial, and will correspond to the item’s index in the collection.
If the block being rendered is not a partial, it will store “object” as a key in an options hash that will be optionally passed to the block when it is rendered. Additionally, “current_index” will store the item’s current index within the collection.
With Options
<% blocks.define :my_block,
a: "First setting of a" %>
<% blocks.define :my_block,
a: "Second setting of a",
b: "First setting of b" %>
<% blocks.define :my_block,
a: "Third setting of a",
b: "Second setting setting of b",
c: "First setting of c" %>
- blocks.define :my_block,
a: "First setting of a"
- blocks.define :my_block,
a: "Second setting of a",
b: "First setting of b"
- blocks.define :my_block,
a: "Third setting of a",
b: "Second setting setting of b",
c: "First setting of c"
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
a: "First setting of a"
builder.define :my_block,
a: "Second setting of a",
b: "First setting of b"
builder.define :my_block,
a: "Third setting of a",
b: "Second setting setting of b",
c: "First setting of c"
After running the above code, the options for :my_block will look like this:
{
a: "First setting of a",
b: "First setting of b",
c: "First setting of c"
}
Blocks are maintaining a merged, mutable options hash. This set is mutated with repeated calls to “define”.
When the same option key is used in successive “define” calls, the first call to define a key’s value is given priority.
Indifferent Access
<% blocks.define :my_block,
a: "First setting of a",
"b" => "First setting of b" %>
<% blocks.define :my_block,
"a" => "Second setting of a",
b: "Second setting of b" %>
- blocks.define :my_block,
a: "First setting of a",
"b" => "First setting of b"
- blocks.define :my_block,
"a" => "Second setting of a",
b: "Second setting of b"
# where builder is an instance of
# Blocks::Builder
builder.define :my_block,
a: "First setting of a",
"b" => "First setting of b"
builder.define :my_block,
"a" => "Second setting of a",
b: "First setting of b"
After running the above code, the options for :my_block will look like this:
{
a: "First setting of a",
b: "First setting of b"
}
Like the name of the block itself, the options hash does not care whether a symbol or a string is provided as a hash key; they are treated the same.
Deep Merging of Options
<% blocks.define :my_block,
a: 1,
shared_key: {
a: "a1"
} %>
<% blocks.define :my_block,
b: 1,
shared_key: {
a: "a2",
b: "b1"
} %>
- blocks.define :my_block,
a: 1,
shared_key: { a: "a1" }
- blocks.define :my_block,
b: 1,
shared_key: { a: "a2",
b: "b1" }
# where builder is an instance of
# Blocks::Builder
builder.define :my_block,
a: 1,
shared_key: {
a: "a1"
}
builder.define :my_block,
b: 1,
shared_key: {
a: "a2",
b: "b1"
}
After running the above code, the options for :my_block will look like this:
{
a: 1,
shared_key: {
a: "a1",
b: "b1"
},
b: 1
}
When the same option key is used in successive “define” calls and the values for the duplicate key are both hashes, they are deep merged, giving precedence for duplicate nested keys to whatever key was defined first.
Runtime and Default Options
<% blocks.define :my_block,
a: "standard",
b: "standard",
runtime: {
a: "runtime",
c: "runtime"
},
defaults: {
a: "default",
d: "default"
} %>
- blocks.define :my_block,
a: "standard",
b: "standard",
runtime: { a: "runtime",
c: "runtime" },
defaults: { a: "default",
d: "default" }
# where builder is an instance of
# Blocks::Builder
builder.define :my_block,
a: "standard",
b: "standard",
runtime: {
a: "runtime",
c: "runtime"
},
defaults: {
a: "default",
d: "default"
}
After running the above code, the options for :my_block will look like this:
{
a: "runtime",
c: "runtime",
b: "standard",
d: "default"
}
There are three levels of options: runtime, standard, and default. They are given merge precedence in that order.
Runtime options are specified as a nested hash with “runtime” as the key.
Default options are specified as a nested hash with “defaults” as the key.
All other keys in the hash are considered standard options.
With Parameters
<% blocks.define :my_block,
a: 1 do |options| %>
My options are <%= options.inspect %>
<% end %>
- blocks.define :my_block,
a: 1 do |options|
My options are
= options.inspect
# where builder is an instance of
# Blocks::Builder
builder.define :my_block,
a: 1 do |options|
"My options are #{options.inspect}"
end
If this block were rendered, the output would be:
My options are { a: 1 }
Every block that is defined with a Ruby block - or a proxy to a Block that is defined with a Ruby block, etc. - can optionally receive the merged options as a parameter.
Without a Name
See Ruby tab
See Ruby tab
# where builder is an instance of
# Blocks::Builder
anonymous_block = builder.define do
"hello"
end
puts anonymous_block.name
# Outputs "anonymous_block_1"
puts anonymous_block.anonymous
# Outputs true
Blocks may be defined without a name. When no block name is provided, an anonymous name will be generated.
Rendering Blocks
<%= blocks.render :my_block %>
<!-- OR -->
<%= blocks.render "my_block" %>
= blocks.render :my_block
#- OR
= blocks.render "my_block"
# where builder is an instance
# of Blocks::Builder
builder.render :my_block
# OR
builder.render "my_block"
There is a single method to render a block that has been defined, regardless of that block’s rendering strategy (whether it’s a Ruby block, Rails partial, or a proxy to another block).
The name of the block being rendered can be a symbol or a string. The underlying system treats symbols and strings the same. Therefore, any block that is defined with a String name can be rendered with its corresponding symbol name and vice-versa.
With no Corresponding Definition
If a block is rendered without a definition, it doesn’t output anything (unless there are hooks or wrappers for the specified block), but it doesn’t fail either.
With a Default Definition
Blocks provides the ability to specify a default definition for the block should no corresponding definition be found.
With a Ruby Block
<%= blocks.render :my_block do %>
content
<% end %>
<!-- OR -->
<% my_block = Proc.new { "content" } %>
<%= blocks.render :my_block,
&my_block %>
<!-- OR -->
<% my_block = Proc.new { "content" } %>
<%= blocks.render :my_block,
defaults: { block: my_block } %>
= blocks.render :my_block do
content
-# OR
- my_block = Proc.new { "content" }
= blocks.render :my_block, &my_block
-# OR
- my_block = Proc.new { "content" }
= blocks.render :my_block,
defaults: { block: my_block }
# where builder is an instance
# of Blocks::Builder
builder.render :my_block do
"content"
end
# OR
my_block = Proc.new { "content" }
builder.render :my_block, &my_block
# OR
my_block = Proc.new { "content" }
builder.render :my_block,
defaults: { block: my_block }
After running the above code, the output will be:
content
The default definition can be specified as a Ruby block:
With a Rails Partial
<%= blocks.render :my_block,
defaults: { partial: "my_partial" } %>
= blocks.render :my_block,
defaults: { partial: "my_partial" }
# where builder is an instance
# of Blocks::Builder
builder.render :my_block,
defaults: { partial: "my_partial" }
After running the above code, the output will be whatever the result is of rendering the partial “my_partial”
The default definition can be specified as a Rails partial:
With a Proxy to Another Block
<%= blocks.render :my_block,
defaults: { with: :proxy_block } %>
= blocks.render :my_block,
defaults: { with: :proxy_block }
# where builder is an instance
# of Blocks::Builder
builder.render :my_block,
defaults: { with: :proxy_block }
After running the above code, the output will be whatever the result is of rendering the Proxy block or method called “some_other_block”
The default definition can be specified as a proxy to another block:
With Options
<%= blocks.render :my_block,
defaults: {
a: "defaults",
b: "defaults"
},
a: "runtime",
c: "runtime" do |options| %>
<%= options.to_json %>
<% end %>
= blocks.render :my_block,
defaults: { a: "defaults",
b: "defaults" },
a: "runtime",
c: "runtime" do |options|
= options.to_json
# where builder is an instance
# of Blocks::Builder
builder.render :my_block,
defaults: {
a: "defaults",
b: "defaults"
},
a: "runtime",
c: "runtime" do |options|
options.to_json
end
The output would be:
{
"a":"runtime",
"c":"runtime",
"b":"defaults"
}
Just as options can be set for a block when the block is defined, they can also be applied at render.
Options provided to the render call can be either runtime options or default options (unlike defining blocks, there is no concept for render standard options).
Default options are specified within a nested hash under the key “defaults”.
All other options are considered to be runtime options. Runtime options provided to the render call will take precedence over all other options, including runtime options set on the block definition.
Indifferent Access
<% blocks.define :my_block,
"a" => "Block String",
b: "Block Symbol" %>
<%= blocks.render :my_block,
a: "Runtime Symbol" do |options| %>
<%= options.to_json %>
<% end %>
- blocks.define :my_block,
"a" => "Block String",
b: "Block Symbol"
= blocks.render :my_block,
a: "Runtime Symbol" do |options|
= options.to_json
# where builder is an instance of
# Blocks::Builder
builder.define :my_block,
"a" => "Block String",
b: "Block Symbol"
builder.render :my_block,
a: "Runtime Symbol" do |options|
options.to_json
end
The output would be:
{
"a":"Runtime Symbol",
"b":"Block Symbol"
}
Note that the render options took precedence over the block options. This is because render options are treated as runtime options (unless they are wrapper inside of the defaults hash) which take the highest level of precedence when merging options.
Like the name of the block itself, the options hash does not care whether a symbol or a string is provided as a hash key; they are treated the same.
Deep Merging of Options
<% blocks.define :my_block,
a: 1,
shared_key: {
a: "a1",
c: "c1"
} %>
<%= blocks.render :my_block,
b: 1,
shared_key: {
a: "a2",
b: "b1"
} do |options| %>
<%= options.to_json %>
<% end %>
- blocks.define :my_block,
a: 1,
shared_key: { a: "a1",
c: "c1"}
= blocks.render :my_block,
b: 1,
shared_key: { a: "a2",
b: "b1" } do |options|
= options.to_json
# where builder is an instance of
# Blocks::Builder
builder.define :my_block,
a: 1,
shared_key: {
a: "a1",
c: "c1"
}
builder.render :my_block,
b: 1,
shared_key: {
a: "a2",
b: "b1"
} do |options|
options.to_json
end
The output would be:
{
"b":1,
"shared_key": {
"a":"a2",
"c":"c1",
"b":"b1"
},
"a":1
}
When the block definition and the render options share a duplicate key with hashes as their values, they are deep merged, giving precedence for duplicate nested keys to the render options.
With Parameters
<% blocks.define :my_block do |param_1, param_2, param_3, param_4| %>
Param 1: <%= param_1.inspect %>
<br>
Param 2: <%= param_2.inspect %>
<br>
Param 3: <%= param_3.inspect %>
<br>
Param 4: <%= param_4.inspect %>
<br>
<% end %>
Without a Collection:
<br>
<%= blocks.render :my_block,
"foo", "bar", a: 1, b: 2 %>
<br>
With a Collection:
<br>
<%= blocks.render :my_block,
"foo", "bar", a: 1, b: 2,
collection: ["Item1", "Item2"] %>
- blocks.define :my_block do |param_1,
param_2, param_3, param_4|
Param 1:
= param_1.inspect
%br
Param 2:
= param_2.inspect
%br
Param 3:
= param_3.inspect
%br
Param 4:
= param_4.inspect
%br
Without a Collection:
%br
= blocks.render :my_block,
"foo", "bar", a: 1, b: 2
%br
With a Collection:
%br
= blocks.render :my_block,
"foo", "bar", a: 1, b: 2,
collection: ["Item1", "Item2"]
# where builder is an instance
# of Blocks::Builder
builder.define :my_block do |param_1,
param_2, param_3, param_4|
"Param 1: #{param_1.inspect}<br>".html_safe +
"Param 2: #{param_2.inspect}<br>".html_safe +
"Param 3: #{param_3.inspect}<br>".html_safe +
"Param 4: #{param_4.inspect}".html_safe +
end
"Without a Collection:<br>".html_safe +
builder.render(:my_block,
"foo", "bar", a: 1, b: 2) +
"<br>With a Collection:<br>".html_safe +
builder.render(:my_block,
"foo", "bar", a: 1, b: 2,
collection: ["Item1", "Item2"])
This will produce the following output
Without a Collection:
Param 1: "foo"
Param 2: "bar"
Param 3: {"a"=>1, "b"=>2}
Param 4: nil
With a Collection:
Param 1: "Item1"
Param 2: "foo"
Param 3: "bar"
Param 4: {"a"=>1, "b"=>2, "object"=>"Item1", "current_index"=>0}
Param 1: "Item2"
Param 2: "foo"
Param 3: "bar"
Param 4: {"a"=>1, "b"=>2, "object"=>"Item2", "current_index"=>1}
Blocks are also capable of taking additional parameters besides potentially the options and the item if a collection is being rendered.
Though this concept is still relatively limited in its potential uses at this time (and some additional use cases probably still need to be hammered out), it can be used to pass an arbitrary number of additional parameters to a defined block.
At this time, this will not do anything meaningful if the Block is defined to render a partial. However, if the Block is defined to be a Ruby block or a Proxy to a Block defined with a Ruby block, Blocks will attempt to match up the number of arguments the block expects with the number of arguments that could be sent to it from the render call.
Remember, options will always be the last argument, and if rendering a collection, item will be the first. Any additional params passed to the render call will then be sent as the second argument onward, followed by the options hash. However, if fewer arguments are expected by the block than are sent, Blocks will send the lesser number of arguments.
See the example to the right to make better sense out of what is being explained here.
With a Collection
<ul>
<%= blocks.render :my_block,
collection: [1, 2, 3, 4] do |item| %>
<li>Item: <%= item %></li>
<% end %>
</ul>
<!-- OR -->
<%= blocks.define :my_block,
collection: [1, 2, 3, 4] %>
<ul>
<%= blocks.render :my_block do |item| %>
<li>Item: <%= item %></li>
<% end %>
</ul>
%ul
= blocks.render :my_block,
collection: [1, 2, 3, 4] do |item|
%li= "Item: #{item}"
-# OR
- blocks.define :my_block,
collection: [1, 2, 3, 4]
%ul
= blocks.render :my_block do |item|
%li= "Item: #{item}"
# where builder is an instance
# of Blocks::Builder
builder.content_tag :ul do
builder.render :my_block,
collection: [1, 2, 3, 4] do |item|
builder.content_tag :li,
"Item #{item}"
end
end
# OR
builder.define :my_block,
collection: [1, 2, 3, 4]
builder.content_tag :ul do
builder.render :my_block do |item|
builder.content_tag :li,
"Item #{item}"
end
end
The output would be:
<ul>
<li>Item: 1</li>
<li>Item: 2</li>
<li>Item: 3</li>
<li>Item: 4</li>
</ul>
A collection may be defined when rendering a block, or it may have already been defined when the block was defined. When the block is rendered, it will actually render the block multiple times, once for each item in the collection.
With an Alias
<ul>
<%= blocks.render :my_block,
collection: [1, 2, 3, 4],
as: :card do |item, options| %>
<li>
Item: <%= item %>
<br>
Options: <%= options.to_hash %>
</li>
<% end %>
</ul>
%ul
= blocks.render :my_block,
collection: [1, 2, 3, 4],
as: :card do |item, options|
%li
="Item: #{item}"
%br
="Options: #{options.to_hash}"
# where builder is an instance
# of Blocks::Builder
builder.content_tag :ul do
builder.render :my_block,
collection: [1, 2, 3, 4],
as: :card do |item, options|
builder.content_tag :li do
"Item #{item}" +
"<br>".html_safe +
"Options: #{options.to_hash}"
end
end
end
The output would be:
<ul>
<li>
Item: 1
<br>
Options: {
"card"=>1,
"current_index"=>0
}
</li>
<li>
Item: 2
<br>
Options: {
"card"=>2,
"current_index"=>1
}
</li>
<li>
Item: 3
<br>
Options: {
"card"=>3,
"current_index"=>2
}
</li>
<li>
Item: 4
<br>
Options: {
"card"=>4,
"current_index"=>3
}
</li>
</ul>
Additionally, you can set an alias for each item in the collection as the collection is iterated over. This is done using the “as” option.
If the block being rendered is a partial, it will alias each item in the collection with the specified option (i.e. the value of the “as” option will become the name of the variable available in the partial being rendered and will contain each item in the collection being rendered). Additionally, “current_index” will also be a variable that can be accessed within the partial, and will correspond to the item’s index in the collection.
If the block being rendered is not a partial, it will store the alias name as a key in an options hash that will be optionally passed to the block when it is rendered. Additionally, “current_index” will store the item’s current index within the collection.
Without an Alias
<ul>
<%= blocks.render :my_block,
collection: [1, 2] do |i, options| %>
<li>
Item: <%= i %>
<br>
Options: <%= options.to_hash %>
</li>
<% end %>
</ul>
%ul
= blocks.render :my_block,
collection: [1, 2] do |i, options|
%li
="Item: #{i}"
%br
="Options: #{options.to_hash}"
# where builder is an instance
# of Blocks::Builder
builder.content_tag :ul do
builder.render :my_block,
collection: [1, 2] do |i, options|
builder.content_tag :li do
"Item #{i}" +
"<br>".html_safe +
"Options: #{options.to_hash}"
end
end
end
The output would be:
<ul>
<li>
Item: 1
<br>
Options: {
"object"=>1,
"current_index"=>0
}
</li>
<li>
Item: 2
<br>
Options: {
"object"=>2,
"current_index"=>1
}
</li>
</ul>
When no alias is specified and the block being rendered is a partial, it will alias each item in the collection as “object” (i.e. “object” will become the name of the variable available in the partial being rendered and will contain each item in the collection being rendered). Additionally, “current_index” will also be a variable that can be accessed within the partial, and will correspond to the item’s index in the collection.
If the block being rendered is not a partial, it will store “object” as a key in an options hash that will be optionally passed to the block when it is rendered. Additionally, “current_index” will store the item’s current index within the collection.
Without a Name
<!-- rendering a partial -->
<%= blocks.render partial:
"a_partial" %>
<!-- with local options -->
<%= blocks.render a: 1, b: 2,
partial: "a_partial" %>
<!-- and a collection -->
<%= blocks.render a: 1, b: 2,
partial: "a_partial",
collection: [1, 2, 3] %>
<!-- rendering with a proxy -->
<%= blocks.render with:
:some_proxy %>
-# rendering a partial
= blocks.render partial: "a_partial"
-# with local options
= blocks.render partial: "a_partial",
a: 1, b: 2
-# and a collection
= blocks.render partial: "a_partial",
a: 1, b: 2, collection: [1, 2, 3]
-# rendering with a proxy
= blocks.render with: :some_proxy
# where builder is an instance
# of Blocks::Builder
# rendering a partial
builder.render partial: "a_partial"
# with local options
builder.render partial: "a_partial",
a: 1, b: 2
# and a collection
builder.render partial: "a_partial",
a: 1, b: 2, collection: [1, 2, 3]
# rendering with a proxy
builder.render with: :some_proxy
Blocks may be rendered without a block name.
This is usually done in combination with a wrapper, a proxy, or a partial.
Use this when you don’t need corresponding hooks for the block to be rendered or when wanting to render a partial or a proxy block.
Hooking Blocks
"before_all" hooks
"around_all" hooks
"around" hooks
"before" hooks
"surround" hook
"prepend" hooks
block
"append" hooks
"after" hooks
"after_all" hooks
Hooks may be registered for a specific block that render additional code in relation to the block when the block is rendered.
There is no limit to the number of hooks that may be registered for a block, and multiple hooks may be registered of the same hook type for a block.
Hooks fall into three categories:
Before Hooks
Before hooks render code before their corresponding block renders.
There are three levels of “before” hooks:
“prepend” Hooks
<% blocks.prepend :my_block do %>
"prepend" call 1
<br>
<% end %>
<% blocks.surround :my_block do |b| %>
"surround" call before
<br>
<%= b.call %>
"surround" call after
<br>
<% end %>
<% blocks.prepend :my_block do %>
"prepend" call 2
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.prepend :my_block do
"prepend" call 1
%br
- blocks.surround :my_block do |b|
"surround" call before
%br
= b.call
"surround" call after
%br
- blocks.prepend :my_block do
"prepend" call 2
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.prepend :my_block do
'"prepend" call 1' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call before' +
builder.content_tag(:br) +
b.call +
'"surround" call after' +
builder.content_tag(:br)
end
builder.prepend :my_block do
'"prepend" call 2' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"surround" call before
"prepend" call 2
"prepend" call 1
"my_block" content
"surround" call after
“prepend” hooks render content that immediately precedes the block content itself.
They render in closest proximity to the block along with the their sibling “append” hooks.
Together with the block content itself and the sibling “append” hooks, they can be surrounded with “surround” calls.
“before” Hooks
<% blocks.before :my_block do %>
"before" call 1
<br>
<% end %>
<% blocks.surround :my_block do |b| %>
"surround" call before
<br>
<%= b.call %>
"surround" call after
<br>
<% end %>
<% blocks.around :my_block do |b| %>
"Around" call before
<br>
<%= b.call %>
"Around" call after
<br>
<% end %>
<% blocks.before :my_block do %>
"before" call 2
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.before :my_block do
"before" call 1
%br
- blocks.surround :my_block do |b|
"surround" call before
%br
= b.call
"surround" call after
%br
- blocks.around :my_block do |b|
"around" call before
%br
= b.call
"around" call after
%br
- blocks.before :my_block do
"before" call 2
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.before :my_block do
'"before" call 1' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call before' +
builder.content_tag(:br) +
b.call +
'"surround" call after' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call before' +
builder.content_tag(:br) +
b.call +
'"around" call after' +
builder.content_tag(:br)
end
builder.before :my_block do
'"before" call 2' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"around" call before
"before" call 2
"before" call 1
"surround" call before
"my_block" content
"surround" call after
"around" call after
“before” hooks render content before “surround” hooks.
Together with the all “surround” content and the sibling “after” hooks, they can be surrounded with “around” calls.
“before_all” Hooks
<% blocks.before_all :my_block do %>
"before" call 1
<br>
<% end %>
<% blocks.around_all :my_block do |b| %>
"around_all" call before
<br>
<%= b.call %>
"around_all" call after
<br>
<% end %>
<% blocks.before_all :my_block do %>
"before_all" call 2
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.before_all :my_block do
"before_all" call 1
%br
- blocks.around_all :my_block do |b|
"around_all" call before
%br
= b.call
"around_all" call after
%br
- blocks.before_all :my_block do
"before_all" call 2
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.before :my_block do
'"before" call 1' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call before' +
builder.content_tag(:br) +
b.call +
'"surround" call after' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call before' +
builder.content_tag(:br) +
b.call +
'"around" call after' +
builder.content_tag(:br)
end
builder.before :my_block do
'"before" call 2' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"before_all" call 2
"before_all" call 1
"around_all" call before
"my_block" content
"around_all" call after
“before_all” hooks render content before anything else, including any “around_all” hooks.
After Hooks
After hooks render code before their corresponding block renders.
There are three levels of “after” hooks:
“append” Hooks
<% blocks.append :my_block do %>
"append" call 1
<br>
<% end %>
<% blocks.surround :my_block do |b| %>
"surround" call before
<br>
<%= b.call %>
"surround" call after
<br>
<% end %>
<% blocks.append :my_block do %>
"append" call 2
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.append :my_block do
"append" call 1
%br
- blocks.surround :my_block do |b|
"surround" call before
%br
= b.call
"surround" call after
%br
- blocks.append :my_block do
"append" call 2
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.append :my_block do
'"prepend" call 1' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call before' +
builder.content_tag(:br) +
b.call +
'"surround" call after' +
builder.content_tag(:br)
end
builder.append :my_block do
'"prepend" call 2' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"surround" call before
"my_block" content
"append" call 1
"append" call 2
"surround" call after
“append” hooks render content that immediately follows the block content itself.
They render in closest proximity to the block along with the their sibling “prepend” hooks.
Together with the block content itself and the sibling “prepend” hooks, they can be surrounded with “surround” calls.
“after” Hooks
<% blocks.after :my_block do %>
"after" call 1
<br>
<% end %>
<% blocks.surround :my_block do |b| %>
"surround" call before
<br>
<%= b.call %>
"surround" call after
<br>
<% end %>
<% blocks.around :my_block do |b| %>
"Around" call before
<br>
<%= b.call %>
"Around" call after
<br>
<% end %>
<% blocks.after :my_block do %>
"after" call 2
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.after :my_block do
"after" call 1
%br
- blocks.surround :my_block do |b|
"surround" call before
%br
= b.call
"surround" call after
%br
- blocks.around :my_block do |b|
"around" call before
%br
= b.call
"around" call after
%br
- blocks.after :my_block do
"after" call 2
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.after :my_block do
'"after" call 1' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call before' +
builder.content_tag(:br) +
b.call +
'"surround" call after' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call before' +
builder.content_tag(:br) +
b.call +
'"around" call after' +
builder.content_tag(:br)
end
builder.after :my_block do
'"after" call 2' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"around" call before
"surround" call before
"my_block" content
"surround" call after
"after" call 1
"after" call 2
"around" call after
“after” hooks render content after “surround” hooks.
Together with the all “surround” content and the sibling “before” hooks, they can be surrounded with “around” calls.
“after_all” Hooks
<% blocks.after_all :my_block do %>
"after_all" call 1
<br>
<% end %>
<% blocks.around_all :my_block do |b| %>
"around_all" call before
<br>
<%= b.call %>
"around_all" call after
<br>
<% end %>
<% blocks.after_all :my_block do %>
"after_all" call 2
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.after_all :my_block do
"after_all" call 1
%br
- blocks.around_all :my_block do |b|
"around_all" call before
%br
= b.call
"around_all" call after
%br
- blocks.after_all :my_block do
"after_all" call 2
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.after_all :my_block do
'"after_all" call 1' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call before' +
builder.content_tag(:br) +
b.call +
'"surround" call after' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call before' +
builder.content_tag(:br) +
b.call +
'"around" call after' +
builder.content_tag(:br)
end
builder.after_all :my_block do
'"after_all" call 2' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"around_all" call before
"my_block" content
"around_all" call after
"after_all" call 1
"after_all" call 2
“after_all” hooks render content after anything else, including any “around_all” hooks.
Around Hooks
Around hooks render code around their corresponding block, allowing the hook to render code before the block renders, pass control over to the rendering block, and then regain control once the block has rendered.
There are three levels of around hooks:
“surround” Hooks
<% blocks.prepend :my_block do %>
"prepend" call
<br>
<% end %>
<% blocks.before :my_block do %>
"before" call
<br>
<% end %>
<% blocks.append :my_block do %>
"append" call
<br>
<% end %>
<% blocks.after :my_block do %>
"after" call
<br>
<% end %>
<% blocks.surround :my_block do |b| %>
"surround" call 1 before
<br>
<%= b.call %>
"surround" call 1 after
<br>
<% end %>
<% blocks.surround :my_block do |b| %>
"surround" call 2 before
<br>
<%= b.call %>
"surround" call 2 after
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.prepend :my_block do
"prepend" call
%br
- blocks.before :my_block do
"before" call
%br
- blocks.append :my_block do
"append" call
%br
- blocks.after :my_block do
"after" call
%br
- blocks.surround :my_block do |b|
"surround" call 1 before
%br
= b.call
"surround" call 1 after
%br
- blocks.surround :my_block do |b|
"surround" call 2 before
%br
= b.call
"surround" call 2 after
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.prepend :my_block do
'"prepend" call' +
builder.content_tag(:br)
end
builder.before :my_block do
'"before" call' +
builder.content_tag(:br)
end
builder.append :my_block do
'"append" call' +
builder.content_tag(:br)
end
builder.after :my_block do
'"after" call' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call 1 before' +
builder.content_tag(:br) +
b.call +
'"surround" call 1 after' +
builder.content_tag(:br)
end
builder.surround :my_block do |b|
'"surround" call 2 before' +
builder.content_tag(:br) +
b.call +
'"surround" call 2 after' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"before" call
"surround" call 2 before
"surround" call 1 before
"prepend" call
"my_block" content
"append" call
"surround" call 1 after
"surround" call 2 after
"after" call
“surround” hooks render content that surround the combination of “prepend” hooks, the block content, and “append” hooks. The can be preceded by “before” hooks and followed by “after” hooks.
“around” Hooks
<% blocks.before :my_block do %>
"before" call
<br>
<% end %>
<% blocks.after :my_block do %>
"after" call
<br>
<% end %>
<% blocks.around :my_block do |b| %>
"around" call 1 before
<br>
<%= b.call %>
"around" call 1 after
<br>
<% end %>
<% blocks.around_all :my_block do |b| %>
"around_all" call before
<br>
<%= b.call %>
"around_all" call after
<br>
<% end %>
<% blocks.around :my_block do |b| %>
"around" call 2 before
<br>
<%= b.call %>
"around" call 2 after
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.before :my_block do
"before" call
%br
- blocks.after :my_block do
"after" call
%br
- blocks.around :my_block do |b|
"around" call 1 before
%br
= b.call
"around" call 1 after
%br
- blocks.around_all :my_block do |b|
"around_all" call before
%br
= b.call
"around_all" call after
%br
- blocks.around :my_block do |b|
"around" call 2 before
%br
= b.call
"around" call 2 after
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.before :my_block do
'"before" call' +
builder.content_tag(:br)
end
builder.after :my_block do
'"after" call' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call 1 before' +
builder.content_tag(:br) +
b.call +
'"around" call 1 after' +
builder.content_tag(:br)
end
builder.around_all :my_block do |b|
'"around_all" call before' +
builder.content_tag(:br) +
b.call +
'"around_all" call after' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call 2 before' +
builder.content_tag(:br) +
b.call +
'"around" call 2 after' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"around_all" call before
"around" call 2 before
"around" call 1 before
"before" call
"my_block" content
"after" call
"around" call 1 after
"around" call 2 after
"around_all" call after
“around” hooks render content that surrounds the combination of “before” hooks, “surround” content, and “after” hooks. They can be surrounded by “around_all” hooks.
“around_all” Hooks
<% blocks.before :my_block do %>
"before" call
<br>
<% end %>
<% blocks.after :my_block do %>
"after" call
<br>
<% end %>
<% blocks.around :my_block do |b| %>
"around" call 1 before
<br>
<%= b.call %>
"around" call 1 after
<br>
<% end %>
<% blocks.around_all :my_block do |b| %>
"around_all" call before
<br>
<%= b.call %>
"around_all" call after
<br>
<% end %>
<% blocks.around :my_block do |b| %>
"around" call 2 before
<br>
<%= b.call %>
"around" call 2 after
<br>
<% end %>
<%= blocks.render :my_block do %>
"my_block" content
<br>
<% end %>
- blocks.before_all :my_block do
"before_all" call
%br
- blocks.after_all :my_block do
"after_all" call
%br
- blocks.around_all :my_block do |b|
"around_all" call 1 before
%br
= b.call
"around_all" call 1 after
%br
- blocks.around :my_block do |b|
"around" call before
%br
= b.call
"around" call after
%br
- blocks.around_all :my_block do |b|
"around_all" call 2 before
%br
= b.call
"around_all" call 2 after
%br
= blocks.render :my_block do
"my_block" content
%br
# where builder is an instance
# of Blocks::Builder
builder.before_all :my_block do
'"before_all" call' +
builder.content_tag(:br)
end
builder.after_all :my_block do
'"after_all" call' +
builder.content_tag(:br)
end
builder.around_all :my_block do |b|
'"around_all" call 1 before' +
builder.content_tag(:br) +
b.call +
'"around_all" call 1 after' +
builder.content_tag(:br)
end
builder.around :my_block do |b|
'"around" call before' +
builder.content_tag(:br) +
b.call +
'"around" call after' +
builder.content_tag(:br)
end
builder.around_all :my_block do |b|
'"around_all" call 2 before' +
builder.content_tag(:br) +
b.call +
'"around_all" call 2 after' +
builder.content_tag(:br)
end
builder.render :my_block do
'"my_block" content' +
builder.content_tag(:br)
end
The output will be:
"before_all" call
"around_all" call 2 before
"around_all" call 1 before
"around" call before
"my_block" content
"around" call after
"around_all" call 1 after
"around_all" call 2 after
"after_all" call
“around_all” hooks render content that surrounds “around” hooks. They can be preceded by “before_all” hooks and followed by “after_all” hooks.
With Options
<% blocks.define :my_block,
a: "Block def",
b: "Block def" %>
<% blocks.around :my_block,
a: "Hook def",
c: "Hook def" do |block, options| %>
Options are <%= options.inspect %>
<%= block.call %>
<% end %>
<%= blocks.render :my_block %>
- blocks.define :my_block,
a: "Block def",
b: "Block def"
- blocks.around :my_block,
a: "Hook def",
c: "Hook def" do |block, options|
Options are
= options.inspect
= block.call
= blocks.render :my_block
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
a: "Block def",
b: "Block def"
builder.around :my_block,
a: "Hook def",
c: "Hook def" do |block, options|
"Options are #{options.inspect} #{block.call}"
end
builder.render :my_block
When rendered, this will produce the following output:
Options are {
"a"=>"Hook def",
"c"=>"Hook def",
"b"=>"Block def"
}
Just as Blocks may be defined with options, so too may hooks be defined with options. When the hook is rendered, these options will take a higher merge precedence than the options that were defined on the block itself. They will however take a lower merge precedence than any render items that were specified when the render call was made for the block being hooked (however, any of the reserved-keywords that are sent to the render call will have already been stripped out).
With a Partial
<% blocks.before :my_block,
partial: "some_partial" %>
- blocks.before :my_block,
partial: "some_partial"
# where builder is an instance
# of Blocks::Builder
builder.before :my_block,
partial: "some_partial"
Hooks may also be defined with Rails partials using the “partial” keyword.
With a Proxy to Another Block
<% blocks.before :my_block,
with: :some_proxy_block %>
- blocks.before :my_block,
with: :some_proxy_block
# where builder is an instance
# of Blocks::Builder
builder.after :my_block,
with: :some_proxy_block
Hooks may also be defined with a proxy to another block using the “with” keyword.
Wrapping Blocks
Wrappers work similarly to hooks with a few notable exceptions:
- Wrappers are singular in nature. While there are three different wrappers that may be applied to a block, only one of each may be applied.
- They are defined directly on the block themselves or provided to the render call.
Wrappers may be defined with either the name of another block, method, or a Proc. That block, method, or Proc must be prepared to take at least one argument, which is the content_block to call when the wrapper is ready to yield control to the content it is wrapping.
“wrap_all” Wrapper
<% blocks.define :wrap_all do |c, o| %>
Wrap All Start
<%= c.call %>
<br>Wrap All End
<% end %>
<% blocks.define :wrap_each do |c, i, o| %>
<% if o.nil?; o = i; i = nil end %>
<br>Wrap Each Start <%= i %><br>
<%= c.call %>
<br>Wrap Each End <%= i %>
<% end %>
<% blocks.around_all :my_block do |c, o| %>
Around All Start<br>
<%= c.call %>
<br>Around All End<br>
<% end %>
<% blocks.define :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each do %>
content
<% end %>
With Collection:
<br>
<%= blocks.render :my_block,
collection: ["a", "b"] %>
<br>
No Collection:
<br>
<%= blocks.render :my_block %>
<!-- OR -->
With Collection:
<br>
<%= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each,
collection: ["a", "b"] %>
<br>
No Collection:
<br>
<%= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each %>
- blocks.define :wrap_all do |c, o|
Wrap All Start
= c.call
%br
Wrap All End
- blocks.define :wrap_each do |c, i, o|
- if o.nil?; o = i; i = nil end
%br
Wrap Each Start
= i
%br
= c.call
%br
Wrap Each End
= i
- blocks.around_all :my_block do |c, o|
Around All Start
%br
= c.call
%br
Around All End
%br
- blocks.define :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each do
content
With Collection:
%br
= blocks.render :my_block,
collection: ["a", "b"]
%br
No Collection:
%br
= blocks.render :my_block
#- OR
With Collection:
%br
= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each,
collection: ["a", "b"]
%br
No Collection:
%br
= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each
# where builder is an instance
# of Blocks::Builder
builder.define :wrap_all do |c, o|
"Wrap All Start" +
c.call +
"<br>Wrap All End".html_safe
end
builder.define :wrap_each do |c, i, o|
if o.nil?
o = i
i = nil
end
"<br>Wrap Each Start #{i}<br>".html_safe +
c.call +
"<br>Wrap Each End #{i}".html_safe
end
builder.around_all :my_block do |c, o|
"Around All Start<br>".html_safe +
c.call +
"<br>Around All End<br>".html_safe
end
builder.define :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each do
"content"
end
"With Collection:<br>".html_safe +
builder.render(:my_block,
collection: ["a", "b"]) +
"<br>No Collection:<br>".html_safe +
builder.render(:my_block)
# OR
"With Collection:<br>".html_safe +
builder.render(:my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each,
collection: ["a", "b"]) +
"<br>No Collection:<br>".html_safe +
builder.render(:my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each)
The above code will output the following:
With Collection:
Around All Start
Wrap All Start
Wrap Each Start a
content
Wrap Each End a
Wrap Each Start b
content
Wrap Each End b
Wrap All End
Around All End
No Collection:
Around All Start
Wrap All Start
Wrap Each Start
content
Wrap Each End
Wrap All End
Around All End
“wrap_all” is a wrapper that surrounds content just inside any “around_all” hooks and around a potential “wrap_each” wrapper (or multiple “wrap_each” wrappers if rendering a collection).
“wrap_each” Wrapper
<% blocks.define :wrap_all do |c, o| %>
Wrap All Start
<%= c.call %>
<br>Wrap All End<br>
<% end %>
<% blocks.define :wrap_each do |c, i, o| %>
<% if o.nil?; o = i; i = nil end %>
<br>Wrap Each Start <%= i %><br>
<%= c.call %>
<br>Wrap Each End <%= i %>
<% end %>
<% blocks.around :my_block do |c, o| %>
Around Start<br>
<%= c.call %>
<br>Around End
<% end %>
<% blocks.define :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each do %>
content
<% end %>
With Collection:
<br>
<%= blocks.render :my_block,
collection: ["a", "b"] %>
<br>
No Collection:
<br>
<%= blocks.render :my_block %>
<!-- OR -->
With Collection:
<br>
<%= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each,
collection: ["a", "b"] %>
<br>
No Collection:
<br>
<%= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each %>
- blocks.define :wrap_all do |c, o|
Wrap All Start
= c.call
%br
Wrap All End
%br
- blocks.define :wrap_each do |c, i, o|
- if o.nil?; o = i; i = nil end
%br
Wrap Each Start
= i
%br
= c.call
%br
Wrap Each End
= i
- blocks.around :my_block do |c, o|
Around Start
%br
= c.call
%br
Around End
- blocks.define :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each do
content
With Collection:
%br
= blocks.render :my_block,
collection: ["a", "b"]
%br
No Collection:
%br
= blocks.render :my_block
#- OR
With Collection:
%br
= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each,
collection: ["a", "b"]
%br
No Collection:
%br
= blocks.render :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each
# where builder is an instance
# of Blocks::Builder
builder.define :wrap_all do |c, o|
"Wrap All Start" +
c.call +
"<br>Wrap All End".html_safe
end
builder.define :wrap_each do |c, i, o|
if o.nil?
o = i
i = nil
end
"<br>Wrap Each Start #{i}<br>".html_safe +
c.call +
"<br>Wrap Each End #{i}<br>".html_safe
end
builder.around :my_block do |c, o|
"Around Start<br>".html_safe +
c.call +
"<br>Around End".html_safe
end
builder.define :my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each do
"content"
end
"With Collection:<br>".html_safe +
builder.render(:my_block,
collection: ["a", "b"]) +
"<br>No Collection:<br>".html_safe +
builder.render(:my_block)
# OR
"With Collection:<br>".html_safe +
builder.render(:my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each,
collection: ["a", "b"]) +
"<br>No Collection:<br>".html_safe +
builder.render(:my_block,
wrap_all: :wrap_all,
wrap_each: :wrap_each)
The above code will output the following:
With Collection:
Wrap All Start
Wrap Each Start a
Around Start
content
Around End
Wrap Each End a
Wrap Each Start b
Around Start
content
Around End
Wrap Each End b
Wrap All End
No Collection:
Wrap All Start
Wrap Each Start
Around Start
content
Around End
Wrap Each End
Wrap All End
Also aliased to “outer_wrapper”
When a collection is involved, the “wrap_each” wrapper will wrap each item in the collection with this wrapper, and all of these wrappers can be wrapped together using the “wrap_all” wrapper.
When no collection is involved, the “wrap_each” wrapper will simply act as another wrapper that can be wrapped within the “wrap_all” wrapper and just outside any “wrap_each” wrappers.
“wrap_with” Wrapper
<% blocks.surround :my_block do |c, i| %>
<% i = nil if i.is_a?(Blocks::RuntimeContext) %>
Surround Start <%= i %><br>
<%= c.call %>
<br>Surround End <%= i %>
<% end %>
<% blocks.define :wrap_with do |c, i| %>
<% i = nil if i.is_a?(Blocks::RuntimeContext) %>
Wrap With Start <%= i %><br>
<%= c.call %>
<br>Wrap Wrap End <%= i %>
<% end %>
<% blocks.before :my_block do |i| %>
<% i = nil if i.is_a?(Blocks::RuntimeContext) %>
Before <%= i %><br>
<% end %>
<% blocks.after :my_block do |i| %>
<% i = nil if i.is_a?(Blocks::RuntimeContext) %>
<br>After <%= i %><br>
<% end %>
<% blocks.define :my_block,
wrap_with: :wrap_with do %>
content
<% end %>
With Collection:
<br>
<%= blocks.render :my_block,
collection: ["a", "b"] %>
<br>
No Collection:
<br>
<%= blocks.render :my_block %>
<!-- OR -->
With Collection:
<br>
<%= blocks.render :my_block,
wrap_with: :wrap_with,
collection: ["a", "b"] %>
<br>
No Collection:
<br>
<%= blocks.render :my_block,
wrap_with: :wrap_with %>
- blocks.surround :my_block do |c, i|
- i = nil if i.is_a?(Blocks::RuntimeContext)
Surround Start
= i
%br
= c.call
%br
Surround End
= i
- blocks.define :wrap_with do |c, i|
- i = nil if i.is_a?(Blocks::RuntimeContext)
Wrap With Start
= i
%br
= c.call
%br
Wrap Wrap End
= i
- blocks.before :my_block do |i|
- i = nil if i.is_a?(Blocks::RuntimeContext)
Before
= i
%br
- blocks.after :my_block do |i|
- i = nil if i.is_a?(Blocks::RuntimeContext)
%br
After
= i
%br
- blocks.define :my_block,
wrap_with: :wrap_with do
content
With Collection:
%br
= blocks.render :my_block,
collection: ["a", "b"]
%br
No Collection:
%br
= blocks.render :my_block
-# OR
With Collection:
%br
= blocks.render :my_block,
wrap_with: :wrap_with,
collection: ["a", "b"]
%br
No Collection:
%br
= blocks.render :my_block,
wrap_with: :wrap_with
# where builder is an instance
# of Blocks::Builder
builder.surround :my_block do |c, i|
i = nil if i.is_a?(Blocks::RuntimeContext)
"Surround Start #{i}<br>".html_safe +
c.call +
"<br>Surround End #{i}".html_safe
end
builder.define :wrap_with do |c, i|
i = nil if i.is_a?(Blocks::RuntimeContext)
"Wrap With Start #{i}<br>".html_safe +
c.call +
"<br>Wrap Wrap End #{i}".html_safe
end
builder.before :my_block do |i|
i = nil if i.is_a?(Blocks::RuntimeContext)
"Before #{i}<br>".html_safe
end
builder.after :my_block do |i|
i = nil if i.is_a?(Blocks::RuntimeContext)
"<br>After #{i}<br>".html_safe
end
builder.define :my_block,
wrap_with: :wrap_with do
"content"
end
"With Collection:<br>".html_safe
builder.render(:my_block,
wrap_with: :wrap_with,
collection: ["a", "b"]) +
"<br>No Collection:<br>".html_safe +
builder.render(:my_block,
wrap_with: :wrap_with)
# OR
"With Collection:<br>".html_safe
builder.render(:my_block,
wrap_with: :wrap_with,
collection: ["a", "b"]) +
"<br>No Collection:<br>".html_safe +
builder.render(:my_block,
wrap_with: :wrap_with)
The above code will output the following:
With Collection:
Before a
Wrap With Start a
Surround Start a
content
Surround End a
Wrap Wrap End a
After a
Before b
Wrap With Start b
Surround Start b
content
Surround End b
Wrap Wrap End b
After b
No Collection:
Before
Wrap With Start
Surround Start
content
Surround End
Wrap Wrap End
After
Also aliased to “wrap”, “wrapper”, and “inner_wrapper”
The “wrap_with” wrapper is preceded by “before” hooks, followed by “after” hooks, surrounded by “around” hooks, and wraps around “surround” hooks.
Defining a wrapper with a Proc
Wrappers can also be defined with Procs. The Proc must take, at a minimum, the content_block that they are wrapping, as the first argument. They may optionally take the options hash as their second argument.
<% wrapper = Proc.new do |b| %>
<div>
<%= b.call %>
</div>
<% end %>
<% blocks.define :my_block,
wrapper: wrapper %>
<%= blocks.render :my_block do %>
Hello
<% end %>
- wrapper = Proc.new do |b|
%div= b.call
- blocks.define :my_block,
wrapper: wrapper
= blocks.render :my_block do
Hello
# where builder is an instance
# of Blocks::Builder
wrapper = Proc.new do |b|
builder.content_tag :div, &b
end
builder.define :my_block,
wrapper: wrapper
builder.render :my_block do
"Hello"
end
The above code will output the following
<div>Hello</div>
Skipping Blocks
Prerequisite code for below examples:
<% blocks.define :my_block,
wrap_all: :wrap_all_wrapper,
wrap_each: :wrap_each_wrapper,
wrap_with: :wrap_with_wrapper do %>
My Block
<br>
<% end %>
<% blocks.define :proxy_block do |*args| %>
<% options = args.extract_options! %>
<% item = args.shift %>
<%= options[:name] %>
<br>
<% end %>
<% blocks.define :wrapper do |b, *args| %>
<% options = args.extract_options! %>
<% item = args.shift %>
<%= options[:name] %> Before
<br>
<%= b.call %>
<%= options[:name] %> After
<br>
<% end %>
<% blocks.define :wrap_all_wrapper,
name: "wrap_all Wrapper",
with: :wrapper %>
<% blocks.define :wrap_each_wrapper,
name: "wrap_each Wrapper",
with: :wrapper %>
<% blocks.define :wrap_with_wrapper,
name: "wrap_with Wrapper",
with: :wrapper %>
<% [:before_all,
:before,
:prepend,
:append,
:after,
:after_all].each do |hook| %>
<% blocks.send(hook,
:my_block,
with: :proxy_block,
name: "\"#{hook}\" Hook") %>
<% end %>
<% [:around_all,
:around,
:surround].each do |hook| %>
<% blocks.send(hook,
:my_block,
with: :wrapper,
name: "\"#{hook}\" Hook") %>
<% end %>
- blocks.define :my_block,
wrap_all: :wrap_all_wrapper,
wrap_each: :wrap_each_wrapper,
wrap_with: :wrap_with_wrapper do
My Block
%br
- blocks.define :proxy_block do |*args|
- options = args.extract_options!
- item = args.shift
= options[:name]
%br
- blocks.define :wrapper do |b, *args|
- options = args.extract_options!
- item = args.shift
= options[:name]
Before
%br
= b.call
= options[:name]
After
%br
- blocks.define :wrap_all_wrapper,
name: "wrap_all Wrapper",
with: :wrapper
- blocks.define :wrap_each_wrapper,
name: "wrap_each Wrapper",
with: :wrapper
- blocks.define :wrap_with_wrapper,
name: "wrap_with Wrapper",
with: :wrapper
- [:before_all,
:before,
:prepend,
:append,
:after,
:after_all].each do |hook|
- blocks.send(hook,
:my_block,
with: :proxy_block,
name: "\"#{hook}\" Hook")
- [:around_all,
:around,
:surround].each do |hook|
- blocks.send(hook,
:my_block,
with: :wrapper,
name: "\"#{hook}\" Hook")
# where builder is an instance
# of Blocks::Builder
builder.define :my_block,
wrap_all: :wrap_all_wrapper,
wrap_each: :wrap_each_wrapper,
wrap_with: :wrap_with_wrapper do
"My Block<br>".html_safe
end
builder.define :proxy_block do |*args|
options = args.extract_options!
item = args.shift
"#{options[:name]}<br>".html_safe +
end
builder.define :wrapper do |b, *args|
options = args.extract_options!
item = args.shift
"#{options[:name]} Before<br>".html_safe +
b.call +
"#{options[:name]} After<br>".html_safe +
end
builder.define :wrap_all_wrapper,
name: "wrap_all Wrapper",
with: :wrapper
builder.define :wrap_each_wrapper,
name: "wrap_each Wrapper",
with: :wrapper
builder.define :wrap_with_wrapper,
name: "wrap_with Wrapper",
with: :wrapper
[:before_all,
:before,
:prepend,
:append,
:after,
:after_all].each do |hook|
builder.send(hook,
:my_block,
with: :proxy_block,
name: "\"#{hook}\" Hook")
end
[:around_all,
:around,
:surround].each do |hook|
builder.send(hook,
:my_block,
with: :wrapper,
name: "\"#{hook}\" Hook")
end
Blocks may be skipped from rendering, such that whenever a render call occurs, no content will be rendered.
Skips come in two forms: skipping a block only, and skipping a block with all of its hooks and wrappers.
Skipping the Block Only
<h2>Render Before Skip:</h2>
<%= blocks.render :my_block %>
<% blocks.skip :my_block %>
<h2>Render After Skip:</h2>
<%= blocks.render :my_block %>
%h2 Render Before Skip:
= blocks.render :my_block
- blocks.skip :my_block
%h2 Render After Skip:
= blocks.render :my_block
output =
"<h2>Render Before Skip:</h2>".
html_safe +
builder.render :my_block
builder.skip :my_block
output +=
"<h2>Render After Skip:</h2>".
html_safe +
builder.render :my_block
output
The above code will output the following:
Render Before Skip:
"before_all" Hook
"around_all" Hook Before
wrap_all Wrapper Before
wrap_each Wrapper Before
"around" Hook Before
"before" Hook
wrap_with Wrapper Before
"surround" Hook Before
"prepend" Hook
My Block
"append" Hook
"surround" Hook After
wrap_with Wrapper After
"after" Hook
"around" Hook After
wrap_each Wrapper After
wrap_all Wrapper After
"around_all" Hook After
"after_all" Hook
Render After Skip:
"before_all" Hook
"around_all" Hook Before
wrap_all Wrapper Before
wrap_each Wrapper Before
"around" Hook Before
"before" Hook
"after" Hook
"around" Hook After
wrap_each Wrapper After
wrap_all Wrapper After
"around_all" Hook After
"after_all" Hook
A block may be skipped using the #skip method.
Calling #skip has the effect of skipping rendering of the block itself, and any “prepend” hooks, “append” hooks, “surround” hooks, and the “wrap_with” wrapper that might have been associated with the block being skipped.
Any “before” hooks, “before_all” hooks, “after” hooks, “after_all” hooks, “around” hooks, “around_all” hooks, the “wrap_all” wrapper, and the “wrap_each” wrapper will continue to be rendered when the block is skipped. See Skipping the Block and its Hooks for skipping everything.
With a Collection
<% blocks.define :my_block,
collection: ["a", "b"] %>
<h2>Render Before Skip:</h2>
<%= blocks.render :my_block %>
<% blocks.skip :my_block %>
<h2>Render After Skip:</h2>
<%= blocks.render :my_block %>
- blocks.define :my_block,
collection: ["a", "b"]
%h2 Render Before Skip:
= blocks.render :my_block
- blocks.skip :my_block
%h2 Render After Skip:
= blocks.render :my_block
output =
"<h2>Render Before Skip:</h2>".
html_safe +
builder.render :my_block
builder.skip :my_block
output +=
"<h2>Render After Skip:</h2>".
html_safe +
builder.render :my_block
output
The above code will output the following:
Render Before Skip:
"before_all" Hook
"around_all" Hook Before
wrap_all Wrapper Before
wrap_each Wrapper Before for item "a"
"around" Hook Before for item "a"
"before" Hook for item "a"
wrap_with Wrapper Before for item "a"
"surround" Hook Before for item "a"
"prepend" Hook for item "a"
My Block
"append" Hook for item "a"
"surround" Hook After for item "a"
wrap_with Wrapper After for item "a"
"after" Hook for item "a"
"around" Hook After for item "a"
wrap_each Wrapper After for item "a"
wrap_each Wrapper Before for item "b"
"around" Hook Before for item "b"
"before" Hook for item "b"
wrap_with Wrapper Before for item "b"
"surround" Hook Before for item "b"
"prepend" Hook for item "b"
My Block
"append" Hook for item "b"
"surround" Hook After for item "b"
wrap_with Wrapper After for item "b"
"after" Hook for item "b"
"around" Hook After for item "b"
wrap_each Wrapper After for item "b"
wrap_all Wrapper After
"around_all" Hook After
"after_all" Hook
Render After Skip:
"before_all" Hook
"around_all" Hook Before
wrap_all Wrapper Before
wrap_each Wrapper Before for item "a"
"around" Hook Before for item "a"
"before" Hook for item "a"
"after" Hook for item "a"
"around" Hook After for item "a"
wrap_each Wrapper After for item "a"
wrap_each Wrapper Before for item "b"
"around" Hook Before for item "b"
"before" Hook for item "b"
"after" Hook for item "b"
"around" Hook After for item "b"
wrap_each Wrapper After for item "b"
wrap_all Wrapper After
"around_all" Hook After
"after_all" Hook
A block with a collection can also be skipped, though the resulting output may not be desired.
Calling #skip on a block with a collection has the effect of skipping rendering of the block itself, and any “prepend” hooks, “append” hooks, “surround” hooks, and the “wrap_with” wrapper that might have been associated with the block being skipped.
Any “before” hooks, “before_all” hooks, “after” hooks, “after_all” hooks, “around” hooks, “around_all” hooks, the “wrap_all” wrapper, and the “wrap_each” wrapper will continue to be rendered when the block is skipped. All the hooks and wrappers, with the exception of the “around_all”, “wrap_all”, “before_all”, and “after_all” ones, will be rendered for each item in the collection (this behavior will likely change in a future release of Blocks).
See Skipping the Block and its Hooks for skipping everything.
Skipping the Block and its Hooks
<% blocks.skip_completely :my_block %>
<%= blocks.render :my_block do %>
Hello
<% end %>
- blocks.skip_completely :my_block
= blocks.render :my_block do
Hello
# where builder is an instance
# of Blocks::Builder
builder.skip_completely :my_block
builder.render :my_block do
"Hello"
end
There will be no output from the above command
Because calling #skip can still have the effect of rendering some of the hooks and wrappers for a particular block (before, after, around, before_all, after_all, wrap_all, wrap_each will still be rendered), there is the need for a second type of skip, called #skip_completely, which will skip the block and all associated hooks and wrappers.
Reserved Keywords
Whether defining a block, defining options for a block, rendering a block, registering hooks for a block, configuring global options for Blocks, or initializing an instance of a Blocks::Builder, there are certain keywords which are reserved for specific purposes:
“collection” and “as” and “object” and “current_index”
“collection” is used to designate a collection for a block. When the block is rendered, it will be rendered for each item in the collection, and the “collection” option will be extracted from the options hash.
“as” is used to give each item in the collection an alias as it is being iterated over. “as” will default to “object”, meaning that “object” becomes a reserved keyword by default. If “as” is set to some other value, whatever that value is will become a reserved keyword for that block.
“current_index” will be a zero-based index of the current item’s position within the collection.
“with”
“with” is used to specify the proxy render strategy, where its value is name of the other block to be rendered in its place.
“partial”
“partial” is used to specify the Rails partial render strategy, where its value is the Rails partial to render.
Other Reserved Keywords when using a partial
http://www.rubymagic.org/posts/ruby-and-rails-reserved-words
“block”
“block” can be used to specify the Ruby block render strategy, where its value is the Ruby block to render. This is an alternative to specifying the block using the “do … end” syntax.
“defaults”
“defaults”, if specified, must have its value be a hash. These represent default options, which are given a lower precedence than standard options when merging options.
“runtime”
“runtime”, if specified, must have its value be a hash. These represent runtime options, which are given a higher precedence than standard options when merging options.
“builder” and “builder_variable”
“builder” will be the default variable name given to the instance of the Blocks::Builder passed to a partial that triggered the call to render to the possible. The “builder” can be used to invoke Blocks functionality within the partial on a shared instance of a Blocks::Builder.
The name “builder” can be overridden by specifying a value in “builder_variable”.
Wrappers
“wrap_all”
Indicates that a wrapper will wrap around any wrap_each wrappers.
“wrap_each” / “outer_wrapper”
Indicates that a wrapper will wrap around a block and any around, before, after, surround, prepend, and append hooks for that block.
“wrap_with” / “wrap” / “wrapper” / “inner_wrapper”
Indicates that a wrapper will wrap around a block and any surround, prepend, and append hooks for that block.
“parent_runtime_context”
Passed internally within the Blocks gem as a parent Blocks::RuntimeContext when calculating the Blocks::RuntimeContext for a hook or wrapper for a block.
Helper Blocks
Blocks comes with some prebuilt utility blocks to aid in your development efforts.
The :content_tag Block
According to Bootstrap’s documentation, a standard card has the following markup:
The :content_tag block is more or less a direct wrapper around ActionView’s content_tag method. However, by defining it as a block, it may be used in several useful ways.
As the definition for a block
<% blocks.define :title, with: :content_tag, tag: :h1, html: { style: "color: red" } %>
<%= blocks.render :title do %>
My Title
<% end %>
- blocks.define :title, with: :content_tag, tag: :h1, html: { style: "color: red" }
= blocks.render :title do
My Title
builder = Blocks::Builder.new(view_context)
builder.define :title, with: :content_tag, tag: :h1, html: { style: "color: red" }
# Since no overrides block is provided, this
# call is synonymous with:
builder.render :title do
"My Title"
end
The output from running the code will be:
<h1 style="color: red">
My Title
</h1>
When used as the definition for a block (using the with keyword), you may choose to specify the tag to use or any html options to be applied to the tag. Then, you can either provide the definition a block of its own or supply as a render option when calling render.
As a wrapper or a surrounding hook for a block
Templating
Sample Syntax
<%= render_with_overrides partial:
"PATH_TO_PARTIAL" do |builder| %>
<!-- Perform overrides here
using the builder.
Whatever happens here
happens first. -->
<% end %>
= render_with_overrides partial: "PATH_TO_PARTIAL" do |b|
#- Perform overrides here using the builder.
#- Whatever happens here happens first.
builder = Blocks::Builder.new(view_context)
builder.render partial:
"PATH_TO_PARTIAL" do |builder|
# Perform overrides here
# using the builder.
# Whatever happens here
# happens first.
end
Templating is one of the most powerful concepts within the Blocks gem. It is the bedrock on which reusable UI components can be built.
A template is nothing more than a Rails partial (in future releases, this concept will likely expand to Ruby blocks as well) that has a reference to an instance of a Blocks::Builder object, and uses it to invoke Blocks functionality. It may consist of multiple block definitions, block render calls, block wrappers and hooks, and other content.
By default, a Blocks::Builder instance will be passed in as a variable called “builder”, but this can be overridden by specifying the “builder_variable” option. When rendering this template, it should produce either a standard / default definition for your template or a sample output of your template complete with dummy data.
However, this standard / default / sample definition can be overridden at runtime with an overrides block that will execute before the template is rendered by the code that is rendering the template.
There are two ways to invoke this functionality, either using the #render_with_overrides method (also aliased as #with_template) that is injected into ActionView as a helper method, or by calling #render_with_overrides on an existing or new instance of a Blocks::Builder.
Building a Bootstrap 4 Card
According to Bootstrap’s documentation, a standard card has the following markup:
<div class="card" style="width: 20rem;">
<img class="card-img-top" src="..." alt="Card image cap">
<div class="card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">
Some quick example text
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
Templating is best demonstrated through example. In the following set of iterative examples, a template for rendering a Bootstrap 4 Card will be defined and expanded upon with Blocks functionality.
Creating a Template
The following code would be added to a new file located at /app/views/shared/_card.html.erb:
<% builder.define :card do %>
<div class="card" style="width: 20rem;">
<img class="card-img-top" src="..." alt="Card image cap">
<div class="card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">
Some quick example text
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
<% end %>
<%= builder.render :card %>
Now, when the template is rendered, it will match the markup above exactly.
<%= render_with_overrides partial: "shared/card" %>
<!-- Since no overrides block is provided, this
call is synonymous with: -->
<%= Blocks::Builder.new(self).render(partial: "shared/card") %>
= render_with_overrides partial: "shared/card"
#- Since no overrides block is provided, this
#- call is synonymous with:
= Blocks::Builder.new(self).render(partial: "shared/card")
builder = Blocks::Builder.new(view_context)
builder.render partial: "shared/card"
# Since no overrides block is provided, this
# call is synonymous with:
builder.render partial: "shared/card"
Setting up a Template is simple. Simply create a Rails partial. That’s the bare minimum that needs to be done, although an empty partial won’t do anything useful.
In this example, we define a block called :card and then render it using the “builder” variable that is automatically defined within the partial.
Our output will match the sample markup above but that is because we have hardcoded the markup within the :card block definition. We’ll need to make the block definitions more dynamic but before we do that, we should extract out more block definitions.
Extracting Out Block Definitions
/app/views/shared/_card.html.erb:
<% builder.define :card do %>
<div class="card" style="width: 20rem;">
<%= builder.render :card_image %>
<%= builder.render :card_content %>
</div>
<% end %>
<% builder.define :card_image do %>
<img class="card-img-top" src="..." alt="Card image cap">
<% end %>
<% builder.define :card_block do %>
<div class="card-block">
<%= builder.render :card_title %>
<%= builder.render :card_text %>
<%= builder.render :card_action %>
</div>
<% end %>
<% builder.define :card_title do %>
<h4 class="card-title">Card title</h4>
<% end %>
<% builder.define :card_text do %>
<p class="card-text">
Some quick example text
</p>
<% end %>
<% builder.define :card_action do %>
<a href="#" class="btn btn-primary">Go somewhere</a>
<% end %>
<% builder.define :card_content,
with: :card_block %>
<%= builder.render :card %>
The output from rendering this template will be the same as before
If you look at the sample markup for a card, hopefully you notice a pattern. It’s something like this:
- A Card is an element with an associated CSS class and is made up of a card image and card content
- A Card image is an image tag with an associated CSS class and an image path
- Card Content is an element with an associated CSS class and is made up of a card title, card text, and a card action
- A Card title is a h4 tag with an associated CSS class and text for the title
- Card text is a p tag with an associated CSS class and text
- A Card Action is a link button with associated CSS classes, a label for the button, and a path for the action.
While not every Bootstrap 4 card will follow this exact pattern, it is a good starting point for beginning to break down a card into pieces.
The code to the right defines the card element and breaks the main :card block into its two components: :card_image and :card_content.
The :card_image block renders the hardcoded image tag and the :card_content block sets itself up to proxy to the :card_block block. This is done in anticipation (based on having read ahead in the Bootstrap 4 Card documentation) of using something other than a card-block for the content of the card (more on this shortly).
Extracting out the Wrappers
/app/views/shared/_card.html.erb:
<% builder.define :block_wrapper,
defaults: {
wrapper_tag: :div,
wrapper_html: {},
wrapper_option: :wrapper_html
} do |block, options| %>
<%= content_tag options[:wrapper_tag],
options[options[:wrapper_option]],
&block %>
<% end %>
<% builder.define :card,
wrapper: :block_wrapper,
wrapper_option: :card_html,
defaults: {
card_html: { class: "card" },
} do %>
<%= builder.render :card_image %>
<%= builder.render :card_content %>
<% end %>
<% builder.define :card_image do %>
<img class="card-img-top" src="..." alt="Card image cap">
<% end %>
<% builder.define :card_block,
wrapper: :block_wrapper,
wrapper_option: :card_content_html,
defaults: {
card_content_html: {
class: "card-block"
}
} do %>
<%= builder.render :card_title %>
<%= builder.render :card_text %>
<%= builder.render :card_action %>
<% end %>
<% builder.define :card_title,
wrapper: :block_wrapper,
wrapper_option: :card_title_html,
wrapper_tag: :h4,
defaults: {
card_title_html: {
class: "card-title",
}
} do %>
Card title
<% end %>
<% builder.define :card_text,
wrapper: :block_wrapper,
wrapper_option: :card_text_html,
wrapper_tag: :p,
defaults: {
card_text_html: { class: "card-text" }
} do %>
Some quick example text
<% end %>
<% builder.define :card_action do %>
<a href="#" class="btn btn-primary">Go somewhere</a>
<% end %>
<% builder.define :card_content,
with: :card_block %>
<%= builder.render :card %>
The output from rendering this template will be the same as before
Perhaps it will also be noticed that every block defined renders an HTML element with nested content. This code is ripe for extraction into a common wrapper block.
In the code to the right, this is exactly what is happening. A new block is defined called :block_wrapper. The :block_wrapper block is setup to take a content_block as its first argument, which will automatically be passed in when :block_wrapper is used as a wrapper for another block. :block_wrapper also defines a couple default options, such as :wrapper_tag being set to :div, :wrapper_html being set to an empty hash, and :wrapper_option being set to :wrapper_html. These options are all used within the :block_wrapper definition, where it builds an HTML element around the content_block. The element will be the :wrapper_tag option type and have attributes specified by the option specified by the :wrapper_option option, which will be :wrapper_html by default.
Because all of :block_wrapper’s options are default options, they are easily overridden by the block being wrapper. For example, the :card block sets wrapper to :block_wrapper and its :wrapper_option to :card_html. Then it sets its default options for :card_html to { class: “card” }. By setting :card_html as a default option for :card, it can easily be overridden by the “render_with_overrides” call that will be demonstrated shortly.
:card_block, :card_title, and :card_text each follow the same paradigm. Notice that :card_text overrides the :wrapper_tag to :p and :card_title overrides it to :h4.
Making the Blocks more Dynamic
/app/views/shared/_card.html.erb:
<% builder.define :block_wrapper,
defaults: {
wrapper_tag: :div,
wrapper_html: {},
wrapper_option: :wrapper_html
} do |block, options| %>
<%= content_tag options[:wrapper_tag],
options[options[:wrapper_option]],
&block %>
<% end %>
<% builder.define :card,
wrapper: :block_wrapper,
wrapper_option: :card_html,
defaults: {
card_html: { class: "card" },
} do %>
<%= builder.render :card_image %>
<%= builder.render :card_content %>
<% end %>
<% builder.define :card_image,
defaults: {
card_image: "placeholder.jpg",
card_image_html: { class: "card-img-top" }
} do |options| %>
<%= image_tag options[:card_image],
options[:card_image_html] %>
<% end %>
<% builder.define :card_block,
wrapper: :block_wrapper,
wrapper_option: :card_block_html,
defaults: {
card_block_html: {
class: "card-block"
}
} do %>
<%= builder.render :card_title %>
<%= builder.render :card_text %>
<%= builder.render :card_action %>
<% end %>
<% builder.define :card_title,
wrapper: :block_wrapper,
wrapper_option: :card_title_html,
wrapper_tag: :h4,
defaults: {
card_title_html: {
class: "card-title",
},
card_title: "Card title"
} do |options| %>
<%= options[:card_title] %>
<% end %>
<% builder.define :card_text,
wrapper: :block_wrapper,
wrapper_option: :card_text_html,
wrapper_tag: :p,
defaults: {
card_text_html: { class: "card-text" },
card_text: "Some quick example text"
} do |options| %>
<%= options[:card_text] %>
<% end %>
<% builder.define :card_action,
defaults: {
card_action_path: '#',
card_action_text: 'Go somewhere',
card_action_html: { class: "btn btn-primary" }
} do |options| %>
<%= link_to options[:card_action_text],
options[:card_action_path],
options[:card_action_html] %>
<% end %>
<% builder.define :card_content,
with: :card_block %>
<%= builder.render :card %>
The output from rendering this template will be the same as before
To round out the Bootstrap 4 Card template, we now specify each block definition that requires dynamic content in it’s definition to take the options hash as a parameter. Any hardcoded context is then moved into the defaults hash for that block as an option and the Blocks gem will take. Where the hardcoded content previously was, we can now replace with dynamic code that utilizes the options hash that is passed in.
Now we have a more or less complete template (though lacking in several features described in the Bootstrap 4 documentation) for rendering and customizing Bootstrap 4 Cards.
Rendering the Template with Option Overrides
<%= render_with_overrides partial: "shared/card",
card_html: { id: "my-card" },
card_action_text: "Go",
card_action_html: { class: "btn btn-danger" },
card_title: "My Title",
card_text: "My Text",
card_action_path: 'http://mobilecause.com',
card_image: "my-image.png" %>
= render_with_overrides partial: "shared/card",
card_html: { id: "my-card" },
card_action_text: "Go",
card_action_html: { class: "btn btn-danger" },
card_title: "My Title",
card_text: "My Text",
card_action_path: 'http://mobilecause.com',
card_image: "my-image.png"
builder = Blocks::Builder.new(view_context,
card_html: { id: "my-card" },
card_action_text: "Go",
card_action_html: { class: "btn btn-danger" },
card_title: "My Title",
card_text: "My Text",
card_action_path: 'http://mobilecause.com',
card_image: "my-image.png")
builder.render partial: "shared/card"
The above code will output the following:
<div class="card" id="my-card">
<img class="card-img-top"
src="/images/my-image.png"
alt="My image" />
<div class="card-block">
<h4 class="card-title">
My Title
</h4>
<p class="card-text">
My Text
</p>
<a class="btn btn-danger"
href="http://mobilecause.com">
Go
</a>
</div>
</div>
Now that the template is defined, we can start rendering it with actual overrides. Since many of the blocks had some of their options defined as defaults, they can easily be overridden by the render_with_overrides options.
Rendering the Template with Block Overrides
<%= render_with_overrides partial:
"shared/card" do |builder| %>
<% builder.define :card do %>
I am a complete replacement for the card
<% end %>
<% end %>
<%= render_with_overrides partial:
"shared/card" do |builder| %>
<%# Change card_title's tag to h2 %>
<% builder.define :card_title,
wrapper_tag: :h2,
card_title: "I had my wrapper tag changed" %>
<%# Change card_action's definition completely %>
<% builder.define :card_action do |options| %>
<button onclick="alert('clicked');">
<%= options[:card_action_text] %>
</button>
<% end %>
<%# turn off card_text's wrapper %>
<%# and change it's definition %>
<% builder.define :card_text, wrapper: nil do %>
This is custom card text.
<% end %>
<% end %>
= render_with_overrides partial: "shared/card" do |builder|
- builder.define :card do
I am a complete replacement for the card
= render_with_overrides partial: "shared/card" do |builder|
-# Change card_title's tag to h2
- builder.define :card_title,
wrapper_tag: :h2,
card_title: "I had my wrapper tag changed"
-# Change card_action's definition completely
- builder.define :card_action do |options|
%button{onclick: "alert('clicked');"}
= options[:card_action_text]
-# turn off card_text's wrapper
-# and change it's definition
- builder.define :card_text,
wrapper: nil do
This is custom card text.
builder = Blocks::Builder.new(view_context)
text = builder.render partial:
"shared/card" do |builder|
builder.define :card do
"I am a complete replacement for the card"
end
end
builder = Blocks::Builder.new(view_context)
text2 = builder.render partial:
"shared/card" do |builder|
# Change card_title's tag to h2
builder.define :card_title,
wrapper_tag: :h2,
card_title: "I had my wrapper tag changed"
# Change card_action's definition completely
builder.define :card_action do |options|
%%"<button onclick='alert('clicked');'>
#{options[:card_action_text]}
</button>%.html_safe
end
# turn off card_text's wrapper
# and change it's definition
builder.define :card_text, wrapper: nil do
"This is custom card text."
end
end
text + text2
The above code will output the following:
<div class="card">
I am a complete replacement for the card
</div>
<div class="card">
<img class="card-img-top"
src="/assets/placeholder.jpg"
alt="Placeholder">
<div class="card-block">
<h2 class="card-title">
I had my wrapper tag changed
</h2>
This is custom card text.
<button onclick="alert('clicked');">
Go somewhere
</button>
</div>
</div>
Finding the option overrides aren’t enough? There’s always block overrides. Any block defined within the template can be overridden by an overrides block that executes before the template is rendered.
Hooking and Skipping Template Definitions
The following code would be added to /app/views/shared/_card.html.erb, just before the builder.render :card call:
<% builder.define :card_subtitle,
wrapper: :block_wrapper,
wrapper_option: :card_subtitle_html,
wrapper_tag: :h6,
defaults: {
card_subtitle_html: {
class: "card-subtitle mb-2 text-muted"
},
card_subtitle: "Card subtitle"
} do |options| %>
<%= options[:card_subtitle] %>
<% end %>
<% builder.define :card_list_group,
wrapper: :block_wrapper,
wrapper_option: :card_list_group_html,
wrapper_tag: :ul,
defaults: {
card_list_group_html: {
class: "list-group list-group-flush"
}
} %>
<% builder.define :card_list_group_item,
wrapper: :block_wrapper,
wrapper_option: :card_list_group_item_html,
wrapper_tag: :li,
defaults: {
card_list_group_item_html: {
class: "list-group-item"
},
card_list_group_item: "Item"
} do |options| %>
<%= options[:card_list_group_item] %>
<% end %>
<% builder.define :card_header,
wrapper: :block_wrapper,
wrapper_option: :card_header_html,
defaults: {
card_header_html: {
class: "card-header"
},
card_header: "Card Header"
} do |options| %>
<%= options[:card_header] %>
<% end %>
<% builder.define :card_footer,
wrapper: :block_wrapper,
wrapper_option: :card_footer_html,
defaults: {
card_footer_html: {
class: "card-footer"
},
card_footer: "Card Footer"
} do |options| %>
<%= options[:card_footer] %>
<% end %>
<%= render_with_overrides partial:
"shared/card" do |builder| %>
<% builder.skip :card_image %>
<% builder.after :card_title do %>
<%= builder.render :card_subtitle %>
<% end %>
<% builder.prepend :card do %>
<%= builder.render :card_header %>
<% end %>
<% builder.append :card do %>
<%= builder.render :card_footer %>
<% end %>
<% builder.define :card_content,
with: :card_list_group %>
<% builder.append :card_content do %>
<%= builder.render :card_list_group_item,
card_list_group_item: "Item 1" %>
<% end %>
<% builder.prepend :card_content do %>
<%= builder.render :card_list_group_item,
card_list_group_item: "Item 0" %>
<% end %>
<% end %>
= render_with_overrides partial: "shared/card" do |builder|
- builder.skip :card_image
- builder.after :card_title do
= builder.render :card_subtitle
- builder.prepend :card do
= builder.render :card_header
- builder.append :card do
= builder.render :card_footer
- builder.define :card_content,
with: :card_list_group
- builder.append :card_content do
= builder.render :card_list_group_item,
card_list_group_item: "Item 1"
- builder.prepend :card_content do
= builder.render :card_list_group_item,
card_list_group_item: "Item 0"
builder = Blocks::Builder.new(view_context)
builder.render partial:
"shared/card" do |builder|
builder.skip :card_image
builder.after :card_title do
builder.render :card_subtitle
end
builder.prepend :card do
builder.render :card_header
end
builder.append :card do
builder.render :card_footer
end
builder.define :card_content,
with: :card_list_group
builder.append :card_content do
builder.render :card_list_group_item,
card_list_group_item: "Item 1"
end
builder.prepend :card_content do
builder.render :card_list_group_item,
card_list_group_item: "Item 0"
end
end
The above code will output the following:
<div class="card">
<div class="card-header">
Card Header
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
Item 0
</li>
<li class="list-group-item">
Item 1
</li>
</ul>
<div class="card-footer">
Card Footer
</div>
</div>
Scanning through the Bootstrap 4 Cards documentation, it can be plainly observed that the current card template only scratches the surface of available features for cards. In the code to the right, a few of these features are added to the template: :card_subtitle, :card_list_group with :card_list_group_item’s, :card_header, and :card_footer. Though all of these features are defined as blocks, none of them are actually used by default. This is actually a good approach, in that these additional features can be added or swapped in as desired while the default output will be unaffected.
We also have access to the full arsenal of hooks, wrapper with relation to the various block definitions within the template. We can also skip blocks or replace the definitions with new definitions.
Extending the Template
Assume the following partial exists in /app/views/shared/_team_card.html.erb:
<%= builder.render partial:
"shared/card" do |builder| %>
<% builder.define :card_title,
card_title: team.name %>
<% builder.define :card_text,
card_text: team.description %>
<% builder.define :card_action,
card_action_text: 'Donate' %>
<% end %>
Then when the following code runs:
<% team = OpenStruct.new(
name: "Andrew's Campaign",
npo: "Innogive",
description: "Donate money"
) %>
<%= render_with_overrides partial:
"shared/team_card",
team: team do |builder| %>
<% builder.after :card_title,
with: :card_subtitle,
card_subtitle: team.npo %>
<% end %>
- team = OpenStruct.new(name: "Andrew's Campaign",
npo: "Innogive",
description: "Donate money")
= render_with_overrides partial: "shared/team_card",
team: team do |builder|
- builder.after :card_title do
= builder.render :card_subtitle,
card_subtitle: team.npo
team = OpenStruct.new(
name: "Andrew's Campaign",
npo: "Innogive",
description: "Donate money"
)
builder = Blocks::Builder.new(view_context)
builder.render partial:
"shared/team_card",
team: team do |builder|
builder.after :card_title do
builder.render :card_subtitle,
card_subtitle: team.npo
end
end
It will produce the following output:
<div class="card">
<img class="card-img-top"
src="/assets/placeholder.jpg"
alt="Placeholder">
<div class="card-block">
<h4 class="card-title">
Andrew's Campaign
</h4>
<h6 class="card-subtitle mb-2 text-muted">
Innogive
</h6>
<p class="card-text">
Donate money
</p>
<a class="btn btn-primary" href="#">
Donate
</a>
</div>
</div>
Templates may also be extended to form new templates, which may also be extended as many times as necessary.
An example might be create new templates that render different versions of a card. For example, maybe one type of card is detailed, while another is an overview. Or maybe one type of card displays information about a team and another about an organization. Or maybe one is meant for conveying information on a dashboard while the other in meant purely for frontend pages. Perhaps then the dashboard card template could be extended multiple times as well to represent different types of objects that may be displayed on the dashboard.
In the example to the right, a team-specific version of card is created as a template. Code then renders this new template with some overrides of if its own. The overrides have been kept very basic in order to clearly demonstrate how to setup a template that extends another template.
Configuration
Blocks is customized by adding an initializer to config/initializers directory called blocks.rb.
Blocks.configure do |config|
# Configuration code goes here
end
Configuring Global Options
Global Options are merged into the set of options generated when rendering a block or hook or wrapper for a block. They are given the lowest precedence when merging options.
Global Runtime Options
Blocks.configure do |config|
config.
set_global_options runtime: {
a: 1, b: 2
}
end
Global Standard Options
Blocks.configure do |config|
config.
set_global_options a: 1, b: 2
end
Global Default Options
Blocks.configure do |config|
config.
set_global_options defaults: {
a: 1, b: 2
}
end
Configuring Caller ID
Blocks.configure do |config|
config.lookup_caller_location = true
end
Caller ID is a debugging feature that is turned off by default, and should really only ever be turned on in Development mode and perhaps Test mode. It is enabled by running the configuration code to the right. Setting this flag to true will noticeably affect the execution speed of page rendering. This is because for every option that is added to a Block, a Proxy to a Block, or when rendering a block, the code will figure out what line of code triggered the setting of that option.
Configuring the Builder Class
Blocks.configure do |config|
config.builder_class = LayoutBuilder
end
The Builder Class is Blocks::Builder by default, but can be changed to something else using this configuration option. Whatever it is changed to, the new class should be a subclass of Blocks::Builder. Configuring the Builder Class is useful when you want to add in block definitions or shared functionality for the entire application.
Custom Builders
class MyCustomBuilder < Blocks.builder_class
def initialize(view, options={})
super view, options
# Additional initialization /
# block definitions could happen here
end
end
# From a controller action:
builder = MyCustomBuilder.new(view_context)
builder.define :some_block do
"Hello"
end
builder.render :some_block
This will output “Hello”
Blocks::Builder is the main class for storing information about block definitions and their corresponding hooks and wrappers.
Wherever a Block is defined, or a hook is registered for a Block, or a block is skipped, or a Block is rendered, all actions will be executed on an instance of Blocks::Builder.
When the “blocks” keyword is called from the view for the first time, it will instantiate a new instance of a Blocks::Builder (or a subclass of Blocks::Builder if the Blocks.builder_class is configured to something else - See Configuring the Builder Class).
A custom builder is just a subclass of Blocks::Builder. Instead of extending the Blocks::Builder class directly though, it is usually better to extend Blocks.builder_class (which will be Blocks::Builder unless it has been configured to something else). The one general exception to this rule is on the class that is configured to be the Blocks.builder_class - this should extend Blocks::Builder directly.
If #initialize is overridden in the subclass, it must, at a minimum call super with a reference to the view or view_context, and optionally, the init options hash.
Custom Builders with Helper Methods
class MyCustomBuilder < Blocks.builder_class
def tag(*args, &block)
options = args.extract_options!
wrapper_html = if options.is_a?(Blocks::RuntimeContext)
wrapper_option =
options[:wrapper_option] || :wrapper_html
options[wrapper_option] || {}
else
options
end
wrapper_tag = options[:wrapper_tag]
if !wrapper_tag
first_arg = args.first
wrapper_tag = if first_arg.is_a?(String) || first_arg.is_a?(Symbol)
first_arg
else
:div
end
end
content_tag wrapper_tag, wrapper_html, &block
end
end
<% builder = MyCustomBuilder.new(self) %>
<%= builder.render :my_block,
wrapper: :tag,
wrapper_tag: :h2,
wrapper_html: { class: 'text-muted' } do %>
Hello
<% end %>
<!-- OR -->
<%= builder.tag :h2, class: 'text-muted' do %>
Hello
<% end %>
- builder = MyCustomBuilder.new(self)
= builder.render :my_block,
wrapper: :tag,
wrapper_tag: :h2,
wrapper_html: { class: 'text-muted' } do
Hello
#- OR
= builder.tag :h2, class: 'text-muted' do
Hello
builder = MyCustomBuilder.new(self)
builder.render :my_block,
wrapper: :block_wrapper,
wrapper_tag: :h2,
wrapper_html: { class: 'text-muted' } do
"Hello"
end
# OR
builder.tag :h2, class: 'text-muted' do
"Hello"
end
This will produce the following output:
<h2 class='text-muted'>Hello</h2>
One of the primary reasons one might want to create a custom builder is to provide helper methods that are isolated within the builder. These helper methods may be invoked directly on the builder object, or indirectly by using a proxy or hook or wrapper.
In the example to the right, tag is declared as a method (which is almost a direct port over of the “block_wrapper” block defined in the Templating Example for Bootstrap Cards it was defined as a Block). The code has been slightly modified to allow the method to be called directly from the builder, or indirectly as a wrapper or hook for a Block.
Acknowledgements
The blocks gem was written and is maintained by Andrew Hunter.
If you think this looks somewhat familiar, there’s good reason for that. Any similarities you may notice with Rails’s content_for with yield and rendering partials in Rails are intentional (the proxying feature may even remind you of Ruby’s Forwardable Module). Part of the original reasoning for the creating this gem was to provide a common interface for rendering both Ruby blocks and Rails’ partials.
Special thanks to the following people for helping advance this gem and its corresponding documentation forward:
- Todd Fisher - For helping me visualize and create the initial inception of this gem back in 2011 and understand how Ruby blocks work
- Jon Phillips - For helping me work out syntax and use cases for the Blocks gem
- Various people and teams at LivingSocial - For allowing and believing in me to run with an experimental and unproven technology to help my gem start to take shape into what it is today.
- Various people at MobileCause - For helping me to advance my vision for this gem into what it is today and what is being documented here.
- Alexis Gormley - For suggesting the Slate theme for the documentation, designing the gem’s branding, and proofing the documentation.
- Vaughn Weiss - For reviewing and revising the Blocks Docs.