Rendering markaby in your helpers

April 2, 2007

Generating markup in your rails helpers is a general practice in rails and is used throughout all rails helpers. Normally you use content_tag to generate markup. But often you will encounter situations, where nested tags force you to write ugly helper code like the following helper method from the rails library:

  1. def options_for_select(container, selected = nil)
  2. container = container.to_a if Hash === container
  3. options_for_select = container.inject([]) do |options, element|
  4. if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last)
  5. is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) )
  6. if is_selected
  7. options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>"
  8. else
  9. options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>"
  10. end
  11. else
  12. is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) )
  13. options << ((is_selected) ? "<option value=\"#{html_escape(element.to_s)}\" selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option value=\"#{html_escape(element.to_s)}\">#{html_escape(element.to_s)}</option>")
  14. end
  15. end
  16. options_for_select.join("\n")
  17. end

Markaby Helper

We will now rewrite this code with inline markaby. We need therefore the following helper method:

  1. def markaby(&proc)
  2. assigns = {}
  3. instance_variables.each do |name|
  4. assigns[ name[1..-1] ] = instance_variable_get(name)
  5. end
  6. Markaby::Builder.new(assigns, self).capture(&proc)
  7. end

We need to collect the instance variables of the current template and pass a hash of instance variable names along with their values to the markaby builder. As second parameter we pass the current template, so that the builder can access other helper methods.

Usage

Ok, let’s rewrite the options_for_select helper. The method takes an array of values which should be displayed as options. Alternatively you may pass an list of pairs like [['first',1],['second',2] or an Hash, which maps from option labels to their values. One thing I did was to refactor the is_selected test into a lambda. It is cleaner to separate the test and probably more efficient. Inside the loop we are testing, if we have pairs or simple values and generate markup by sending the option method to the builder, which causes the markaby builder to generate an option tag. Tag attributes are defined with a hash, which we pass to the option method. A tag method takes an optional block, which defines the content of a tag, in our case simply the text of the option.

  1. def options_for_select(container, selected = nil)
  2. container = container.to_a if Hash === container
  3. if selected.respond_to?(:include?) and !selected.is_a?(String)
  4. is_selected = lambda { |e| selected.include? e }
  5. else
  6. is_selected = lambda { |e| selected == e }
  7. end
  8. is_pair = lambda {|e| !e.is_a?(String) and e.respond_to?(:first) and e.respond_to?(:last) }
  9. markaby do
  10. container.each do |element|
  11. if is_pair[element]
  12. if is_selected[element.last]
  13. option(:value => element.last, :selected => 'selected') { h element.first }
  14. else
  15. option(:value => element.last) { h element.first }
  16. end
  17. else
  18. if is_selected[element]
  19. option(:value => element, :selected => 'selected') { h element }
  20. else
  21. option(:value => element) { h element }
  22. end
  23. end
  24. end
  25. end
  26. end

Reusable helpers

Our defined markaby method is even more useful, we can accept a block for our helper method and use it inside the markaby code:

  1. def tasks(&block)
  2. markaby do
  3. div.tasks {
  4. ul {
  5. markaby(&amp;block)
  6. }
  7. }
  8. end
  9. end

If we have a common pattern like a list of tasks for many templates, we can generate the common code with the tasks method and put the actual tasks in the block:

  1. tasks {
  2. task 'Back to articles'.t, articles_url
  3. task :edit, @article
  4. task :versions, @article
  5. }

So, you can see, there is also a task helper, which is defined as follows:

  1. def task(text, url_or_resource, html_options={})
  2. if text.is_a? Symbol
  3. task "#{text.to_s.humanize}".t, {:action => text, :id => url_or_resource}, html_options
  4. else
  5. markaby { li { link_to text, url_or_resource, html_options } }
  6. end
  7. end

If the link text is a symbol, we are going to infer the url from the action name which is the first parameter and the recource, which is the second parameter in this case. Otherwise we generate a list element and delegate the arguments to the link_to helper.

By using this simple abstraction, we have hidden the details of task links. Instead of repeating the same pattern over and over again, we have a common place to decide, how the tasks should look like. Markaby makes it really easy to generate nested structures, as it takes advantage of ruby’s block syntax.


Posted in category Ruby by Matthias Georgi. Tagged with markaby.
Similar Posts