This post originated from an RSS feed registered with Ruby Buzz
by Matthias Georgi.
Original Post: Rendering markaby in your helpers
Feed Title: Matthias Georgi
Feed URL: http://feeds.feedburner.com/matthias-georgi?format=xml
Feed Description: Webdev, Gamedev and Interaction Design.
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:
options_for_select = container.inject([])do |options, element| if !element.is_a?(String)and element.respond_to?(:first)and element.respond_to?(:last) is_selected =((selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected)) if is_selected options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>" else options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>" end else is_selected =((selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected)) 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>") end end
options_for_select.join("\n") end
We will now rewrite this code with inline markaby. We need therefore the following helper method:
def markaby(&proc) assigns ={} instance_variables.eachdo |name| assigns[ name[1..-1]]= instance_variable_get(name) end Markaby::Builder.new(assigns, self).capture(&proc) 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.
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.
if selected.respond_to?(:include?)and !selected.is_a?(String) is_selected =lambda{ |e| selected.include? e } else is_selected =lambda{ |e| selected == e } end
markaby do container.eachdo |element| if is_pair[element] if is_selected[element.last] option(:value => element.last, :selected => 'selected'){ h element.first } else option(:value => element.last){ h element.first } end else if is_selected[element] option(:value => element, :selected => 'selected'){ h element } else option(:value => element){ h element } end end end end end
Our defined markaby method is even more useful, we can accept a block for our helper method and use it inside the markaby code:
def tasks(&block) markaby do div.tasks { ul { markaby(&block) } } end 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:
So, you can see, there is also a task helper, which is defined as follows:
def task(text, url_or_resource, html_options={}) if text.is_a? Symbol task "#{text.to_s.humanize}".t, {:action => text, :id => url_or_resource}, html_options else markaby { li { link_to text, url_or_resource, html_options }} end 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, which is really DRY. 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.
In a future post I will discuss an advanced form builder, like the one already implemented in Rails, but this time implemented with the help of Markaby. It is even possible to separate form generation and form layouting by using a separate form renderer. So step by, if you want to delve into further Markaby Magic.