Using Ruby Blocks to make custom helpers in Rails

Posted by Brian in Rails, snacks (July 2nd, 2007)

A ruby block is one of the most powerful things about the language. I think it’s one of the most important concepts in the entire language.

To illustrate the point, I’ll explain blocks a bit by using an example we can all relate to.

Have you ever written code that looks like this?

<% if @documents.size > 0 %>
   <% documents.each do |@document| %>
      

<%=@document.title %>

<% end %> <% else %>

There are no items to show.

<% end %>

You check to see if any results are in the collection so you can display a nice friendly message to people saying
that you didn’t find anything.

We can use Ruby’s blocks to shorten that code and make it a bit more generic.

What are blocks?

Methods in Ruby can take arguments (or parameters, if you prefer),
but they can also take blocks of code. Ruby can then execute this
code you pass in within the scope of the method. That sounds pretty abstract,
but it’s nothing more than just allowing your code to be wrapped by some other code.

Using a helper that accepts your code as a block, you can do something like this in one of your views:

<% display_items_from @documents, "There are no documents to display" do |document| %>
   

<%=link_to document, :action=>"show", :id=>document %>

<% end %>

We expect that this method will check the size of the @documents collection, and if it has results, it will loop over the collection of documents and then execute the block of code we pase in. If the collection of documents is empty, we will display the message.

The first iteration – Displaying the items

The code is actually really simple to implement. Let’s handle the easy case first… the case
where we actually have documents to show.

def display_items_from(collection, blank_message="There is nothing to display")
	if collection.size > 0
		collection.each do |item|
			yield item
		end
	end
end

This is where Ruby gets really abstract. That method declaration doesn’t mention anything about handling a block! It turns out that I can pass a block to any method, and Ruby will use it if I use the yield keyword. I don’t have to declare the block as a parameter in most cases.

When I do

	<% display_items_from @documents, "There are no documents to display" do |document|| %>
		

<%=link_to document.title, :action=>"show", :id=>document %>

<% end %>

in my view, I am passing document in as a local variable that the helper method can bind to.

Look at this section of code:

		collection.each do |item|
			yield item
		end

The yield statement is what does the actual output. I specified

		

<%=link_to document, :action=>"show", :id=>document %>

so this will execute the block of code I passed in for each item in the collection,
binding item with my local variable document and using yield to execute the block of code I passed.

To reiterate, you use blocks in Ruby to run a bunch of code in the context and scope of another method.

The other case – Nothing to return

Rails has a few wonderful helper methods we can use to make this helper more useful. Right now, you can get the same result by using a partial with the :collection parameter.
But what we really want is a nice, clean way to iterate over our collection if it has items, and display a friendly message to our users if there were no items in the collection.

You’d think that would be easy… I could just do

def display_items_from(collection, blank_message="There is nothing to display")

	if collection.size > 0
		collection.each do |item|
		   	yield item
		end
	else
		blank_message
	end

end

There’s a slight problem with that approach though… I will never see that blank message on the screen anywhere. Remember, this type of
helper method is invoked using the regular ERb evaluation mechanism of <% .. %> and not the ERb output
mechanism (<%= .. %>). There’s nothing in our code that will output blank_message variable.

Ok, so what if I did this?

  else
  	yield blank_message
  end

Would that work?

No, of course not. Yield operates on the stuff I passed in, so it’s going to try to call document.title and I’ll get an exception.

It turns out the solution requires the use of the concat method. The concat method takes two parameters: the string to output, and the block or proc to bind the output to.

	else
		concat(blank_message, block.binding)
	end

There’s just one small problem…. I’ll get an exception when I call this because
block was never defined. Remember when I said that you don’t have to declare
that you’re passing a block to a method in Ruby, and that it will ignore the block of code you pass if
the method doesn’t call for it? Well, that’s true, except in the case where you actually need to do
something with the block, like bind to it.

All I have to do is change the method declaration just slightly by adding
&block as the third parameter. The ampersand is what specifies the block,
and it absolutely must come last in the list of method arguments.

Our updated helper method now looks like this:

def display_items_from(collection, blank_message="There is nothing to display", &block)
	if collection.size > 0
		collection.each do |item|
		   	yield item
		end
	else
		concat(blank_message, block.binding)
	end
end

Handling Errors

We should always make sure that this method is called with a block. We can use the built-in method called block_given? which returns false if the developer
didn’t pass a block to the method.

    raise ArgumentError, "You need to provide a block." unless block_given?

You could make that more helpful by placing some nice instructions in that message.

Displaying a record count

If I wanted to display a count of the number of results you found in the output above the
code I passed, all I have to do is make
use of the concat method again, and place it above the yield statement.

		concat("

You have #{collection.size} items.

", block.binding) collection.each do |item| yield item end

Wrapping up

The final method looks like this:

def display_items_from(collection, blank_message="There is nothing to display", &block)

    raise ArgumentError, "You need to provide a block." unless block_given?

	if collection.size > 0
		concat("

You have #{collection.size} items.

", block.binding) collection.each do |item| yield item end else concat(blank_message, block.binding) end end

Now that I’ve covered this in more detail, the wheels should be spinning in your head. Think about
all of the possible helpers you could write to reduce your view code?

  • Easily make a navigation bar of links
  • Create a helper to assist with the creation of rounded-corner areas
  • Come up with a neat mechanism to show admin-only content

I hope this was helpful. Don’t hesitate to ask questions in the comments. If there’s anything I can make more clear,
let me know and I’ll do what I can. I was extremely influenced by Bruce Williams and Marcel Molina Jr.’s presentation at RailsConf. You can read the
slides at http://www.codefluency.com/assets/2007/5/18/VisForVexing.pdf

5 Responses to ' Using Ruby Blocks to make custom helpers in Rails '

Subscribe to comments with RSS or TrackBack to ' Using Ruby Blocks to make custom helpers in Rails '.

  1. Chris said,
    on July 4th, 2007 at 10:06 am

    I’m impressed. This provides a nice way for me to hack out good chunk of repetitive generic code from my views. Thanks for this Brian!

  2. on July 4th, 2007 at 11:34 am

    I love the way code looks by using these types of helpers. I have used blocks in the past to also use it to house some complex, nasty HTML, like for having a rounded corner piece. I have done a write up on it at http://shifteleven.com/articles/2007/01/22/blocks-and-helpers-a-lovely-combination

    Good read

  3. Navjeet said,
    on July 5th, 2007 at 6:23 am

    This is real good and very useful stuff. :smile:

  4. Shadowfiend said,
    on July 5th, 2007 at 6:58 am

    Just a mild correction — you said that concat takes a proc or block as its second parameter, rather than a Binding object. The Binding object can come from anywhere, it’s just that the block’s binding is the one that’s very useful in these circumstances.

  5. Mike W said,
    on January 30th, 2008 at 9:47 am

    Thanks Brian! The section on yield helped me to clean up my helpers. I was able to go from three separate helpers to just one.

Leave a reply

:mrgreen: :neutral: :twisted: :shock: :smile: :???: :cool: :evil: :grin: :oops: :razz: :roll: :wink: :cry: :eek: :lol: :mad: :sad: