10

Blocks and Helpers, a Lovely Combination

Posted in Ruby on Rails at January 22nd, 2007 /

Blocks rock and I couldn’t agree more. The functional programming aspect of ruby has started to interest me more and more. Blocks are used all over the place, with respond_to, collect, returning, and ActionView::Helpers::FormHelper#form_for.

Sometimes, a design requires a little extra markup, perhaps something to get that rounded corner to work or whathave you. Now let’s say this piece of code requires certain classes, a certain kind of structure, including a tile, and has to be used in many places, something like a sidebar piece. Instead of copying and pasting this structure over and over, you can use a helper to make you life easier, and if you combine it with a block, it will just feel even more painless.

The Setup

Pretend this is the HTML in question.

  <%= sidebar(
    'Hello World',
    '<ul>' +
      '<li>Item</li>' +
      '<li>Item</li>' +
      '<li>Item</li>' +
    '</ul>'
  ) %>

This feels pretty klunky and ERB is completely ignored. Instead of this procedural way of thinking, why not see what a block can do for us?

Creating a Helper with a Block

For this to work, we will still need the first parameter to be the title, but the body will be passed in as a block to be processed from within our helper. The block we create will be ERB code, this will feel more natural to the person who has to implement the code. The magic to get this done comes from the concat method of ActionView::Helpers::TextHelper module.

  def sidebar(title, &block)
    raise ArgumentError, "Missing block" unless block_given?
    
    start = <<-END
      <div class="sidebar">
        <div class="bottom">
          <h3>#{title}</h3>
          <div class="body">
    END
    concat(start, block.binding)
      
    block.call
    
    end = <<-END
          </div>
        </div>
      </div>
    END
    concat(end, block.binding)
    
  end

This may look slightly odd, but I assure you, the interface makes up for it.

  <% sidebar('Hello World') do %>
    <p>And now back to the way I was doing things before</p>
    <ul>
      <li>Item</li>
      <li>Item</li>
      <li>Item</li>
    </ul>
  <% end %>

The Benefits

  • The implementation code is more readable
  • It gives you one place to make your common edits
  • It keeps your code DRY
Published in Ruby on Rails

10 Responses to “Blocks and Helpers, a Lovely Combination”

  1. January 23rd, 2007 at 8:27 am #Steve

    This is really nice. Thanks for the tutorial. The only thing that it would be nice to clean up is the html inside the helper. Having to concatenate strings of html together is kind of tough to read.

  2. January 23rd, 2007 at 9:28 am #Logan Capaldo

    Pretty sure you mean “interface” when you say “implementation”. (The implementation would be the code in def sidebar …)

  3. January 23rd, 2007 at 9:47 am #Branden Timm

    How about something like this, to really embrace the functionality :) ? Where param html is an array of first-level div lines…

    def create_sidebar(html, &block)

      raise ArgumentError, “Missing block” unless block_given?

      case html.length

      when 1

        concat(html0, block.binding)

        block.call

        concat(’</div>’, block.binding)

      else

        concat(html0, block.binding)

        create_sidebar(html[1..html.length], block)

        concat(’</div>’, block.binding)

      end

    end

    My ruby is a little rusty so this isnt the most elegant implementation im sure…but the point remains that it feels natural to write nested div elements using a recursive algorithm.

  4. January 23rd, 2007 at 10:05 am #Adam

    Steve: Good point. I will fix that up so I’m not doing so many concat calls

    Logan: You’re right. I had my terms mixed up. Thanks for the heads up.

  5. January 23rd, 2007 at 11:18 am #Jules

    Rails already supports this: content_for :sidebar do ... end

  6. January 23rd, 2007 at 11:46 am #John

    I’m having trouble understanding:
    concat(start, block.binding)

    Wouldn’t this append start after the block’s text? If it doesn’t, then how is it possible that this works?
    concat(end, block.binding)

    wouldn’t the end go at the front of it all? sorry, somewhat confused by concat’s and bindings.

  7. January 23rd, 2007 at 12:43 pm #Adam

    Branden: That would be a good idea to create a generic implementation, more or less. With a little tweaking that could be awesome.

    Jules: there is that, but it’s not something that can be cached as a fragment.

    John: Look at the concat method. It’s more of a function of Binding than it is a String’s concat. I got this syntax from rails’ form_for method and I’m not 100% sure how Bindings work as of yet.

  8. January 23rd, 2007 at 5:51 pm #Ron Green

    Thanks for this post! I was just looking for a way to encapsulate the opening and closing div elements needed for a rounded box design. This keeps those elements coupled whle yielding for the div contents! Thanks!

  9. January 24th, 2007 at 3:09 pm #Ronie Uliana

    You can use these 4 lines in your ApplicationHelper:

    def with_partial(name, options = nil, &block)
      content_for(name.to_sym, &block)
      concat(render(:partial => name.to_s, :locals => options), block.binding)
    end
    

    Then your partial could be:

      <% with_partial :sidebar, :title => 'Hello word' do %>
        <p>And now back to the way I was doing things before</p>
        <ul>
          <li>Item</li>
          <li>Item</li>
          <li>Item</li>
        </ul>
      <% end %>
    

    And the partial _sidebar.rhtml could be:

      <div class="sidebar">
        <div class="bottom">
          <h3><%= title %></h3>
          <div class="body">
            <%= yield :sidebar %>
          </div>
        </div>
      </div>
    
  10. January 25th, 2007 at 12:04 pm #Adam

    That’s an interesting solution as well; however, it has an unwanted side-effect: If you use the same with_partial :sidebar in more than one place on your page, you end up with duplicate information.

    The first time you call it, it works great. The second time you call it, it prints out the data from the first block AND the second block. The third time it renders the block information from the first call AND the second call AND the third; and so on and so on.

Leave a Reply