Form Element Objects in Merb

Posted: April 8th, 2009 | Author: Daniel Higginbotham | Filed under: Uncategorized |

Overview

I’ve been trying to make merb a little bit easier to use by implementing form element classes. The approach I’ve taken is influenced by my experience with Cocoa. The view classes I’ve created encapsulate behavior for displaying complex form elements and for parsing the data sent to controllers by the form elements.

One great advantage that Cocoa development has over web development with an MVC framework like merb is that your views are first-class objects and can be communicated with directly using the same language as the rest of the system. With a web app, you have to go to extra lengths so that your Model or Controller will correctly get data from a form element if the element is even slightly complex. Most likely you’ll need to use Javascript in addition to whatever backend language you’re using. You’ll probably also have a lot of code to parse those elements in your controllers, spreading the concept your form element represents all over the place. In Cocoa, interacting with complex “form elements” is easier and cleaner.

Brief Cocoa Example

One iPhone app I’m on working on stores a time interval in seconds. Since it’s not very user-friendly to make a user figure that out, I use a picker that allows him to specify days, hours, and minutes.

Picker for days, hours, and minutes

This picker is an instance of a subclass of UIPickerView, which means it’s a first-class object and I can define methods on it to get at its tasty insides. The advantage here is that methods that belong together conceptually are placed together physically. The salient method is below:

picker valueInSeconds

  1. - (NSInteger)valueInSeconds
  2. {
  3.         NSInteger dayRow = [self selectedRowInComponent:0];
  4.         NSInteger hourRow = [self selectedRowInComponent:1];
  5.         NSInteger minuteRow = [self selectedRowInComponent:2];
  6.         NSInteger daySeconds = dayRow * 24 * 60 * 60;
  7.         NSInteger hourSeconds = hourRow * 60 * 60;
  8.         NSInteger minuteSeconds = minuteRow * 5 * 60;
  9.        
  10.         return (daySeconds + hourSeconds + minuteSeconds);
  11. }

Therefore when I’m ready to set the time interval, I just do the following:

medication.interval = [intervalPicker valueInSeconds];

From what I understand, this is all nothing special in Cocoa development.

The Web App Problem

merb (and Rails) have no mechanism for treating form elements as objects. Form elements are displayed using javascript, templates, and helpers. Then their data is sent to a controller as a hash. They’re usually parsed with code in the controller.

For example, traineo.com we have the following form elements:

American units, weight in lbs

British units, weight in stone

For these form elements, we use javascript to change the weight input when the user clicks a radio button.

Initially, we had some code in our controller to get the “weight” value and convert it to kilograms so that we could then pass it to a model. Something like

if (params[:weight_input]["stone"])
# Convert from stone to kg
elsif (params[:weight_input]["american"])
# Convert from lbs to kg
else
# Leave as is; already in kg

This worked ok, but once we started placing the weight input fields in other forms, the code had to be improved. We could have created a method in ApplicationController for parsing date input, but it didn’t seem like good OO programming to make ApplicationController aware of and responsible for one set of form fields. Better to make a class and take advantage of Ruby’s object goodness.

Enough of my blathering - here’s the code:

merb widget

  1. # lib/widgets/widget.rb
  2. # Superclass for our form element "widgets"
  3. class Widget
  4.   include Merb::GlobalHelpers
  5.  
  6.   # This is necessary to include Merb::GlobalHelpers
  7.   class_inheritable_accessor :_default_builder
  8.  
  9.   # These are for convenience
  10.   attr_accessor :params
  11.   attr_accessor :session
  12.  
  13.   def initialize(params = {}, session = {})
  14.     self.params = params
  15.     self.session = session
  16.   end
  17.  
  18.   def value
  19.   end
  20. end
  21.  
  22. # lib/widgets/weight_input_switcher.rb
  23. class WeightInputSwitcher < Widget
  24.   attr_accessor :attribute_name, :weight_in_kg, :unit_preference
  25.  
  26.   # View methods
  27.   def setup(attribute_name, weight_in_kg = nil, unit_preference = session[:unit_preference])
  28.     self.attribute_name = attribute_name
  29.     self.weight_in_kg = weight_in_kg
  30.     self.unit_preference = unit_preference
  31.     self
  32.   end
  33.  
  34.   def switcher
  35.     html = "<div class='weight_input_switcher'>"
  36.     html += weight_input("lbs")
  37.     html += weight_input("kg")
  38.     html += weight_input("st")
  39.     html += "</div>"
  40.     html
  41.   end
  42.  
  43.   def weight_input
  44.     html = "<span class='weight_input #{unit_preference}'>"
  45.     html += weight_input_field
  46.     html += "</span>"
  47.   end
  48.  
  49.   def weight_input_field
  50.     if unit_preference == "st"
  51.       stone, lbs = if weight_in_kg.to_i == 0
  52.         ["",""]
  53.       else
  54.         weight_input_value(weight_in_kg, unit_preference).split(" st ")
  55.       end
  56.  
  57.       html = text_field attribute_name, :value => stone, :name => "weight_input[stone]", :class => "weight_input_field stone"
  58.       html += "<span class='weight_unit'>stone</span>"
  59.       html += text_field attribute_name, :value => lbs, :name => "weight_input[lbs]", :class => "weight_input_field stone lbs"
  60.       html += "<span class='weight_unit'>lbs</span>"
  61.     else
  62.       value = weight_input_value(weight_in_kg, unit_preference)
  63.      
  64.       html = text_field attribute_name, :value => value, :name => "weight_input", :class => "weight_input_field"
  65.       html += "<span class='weight_unit'>" + unit_preference.to_s + "</span>"
  66.     end
  67.     html
  68.   end
  69.  
  70.   # Parse methods
  71.   # We kinda cheat here and use params. Need to figure out a better way to do this.
  72.   def value
  73.     if params["weight_input"]["stone"]
  74.       "#{params["weight_input"]["stone"]} st #{params["weight_input"]["lbs"]} lbs"
  75.     else
  76.       "#{params["weight_input"]} #{Units.weight_full_to_abbreviation_map[unit_preference]}"
  77.     end
  78.   end
  79.  
  80.   private
  81.   def weight_input_value(weight_in_kg, unit_preference)
  82.     begin
  83.       weight_in_kg.to_display_weight_without_unit(unit_preference)
  84.     rescue
  85.       ""
  86.     end
  87.   end
  88. end
  89.  
  90. # The following is added to global_helpers.rb
  91. def widget(klass, *args)
  92.   klass.new(params, session).setup(*args)
  93. end
  94.  
  95. # An example of using the "Widget" to display form elements
  96. widget(WeightInputSwitcher, :weight, @current_user.current_weight).switcher
  97.  
  98. # An example of using the "Widget" to get the value of form elements
  99. weight_widget = widget(WeightInputSwitcher, :weight, 0, "", user[:unit_preference])
  100. weight = weight_widget.value

So my code could use some improvement, but I think it lays some good groundwork for treating form elements as objects in Merb. Any feedback would be greatly appreciated!


3 Comments on “Form Element Objects in Merb”

  1. 1 Man said at 7:20 am on April 8th, 2009:

    Cool!

  2. 2 Steve Eley said at 10:57 pm on April 12th, 2009:

    Some of your code was pretty hard to follow. Did you rename “display” to “switcher” between writing the abstract class and subclass? And the last two lines seem confusing. I don’t see where weight_widget is defined.

    Beyond that… This seems like a manifestation of the Presenter pattern (http://blog.jayfields.com/2007/03/rails-presenter-pattern.html and http://caboose.org/articles/2007/8/23/simple-presenters), with possibly a focus on returning atomic values. Cool enough, but I think the calling syntax needs a bit of work. The generic “widget” factory taking a classname, and then having to chain ’switcher’ (display?) onto it, doesn’t seem very first-class.

  3. 3 Daniel Higginbotham said at 7:18 am on April 14th, 2009:

    Hi Steve,

    I apologize for how confusing the code was. I took out the “display” method from the abstract class because I found that the “widget” could have multiple display methods. This should be updated in the code sample. I’ve also defined weight_widget at the bottom of the code sample.

    Thanks for for the Presenter pattern links. They’ve helped crystallize my thinking on this. One difference is that my “Presenter” also handles the data sent to the controller. I think this is what you meant by “a focus on returning atomic values”?

    And you’re right, the way “widgets” are created and called is pretty lame. Right now the purpose is to set the “session” and “params” variables every time without having to do so explicitly. I’m sure there’s a better way to do so.


Leave a Reply