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.
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:
- - (NSInteger)valueInSeconds
- {
- NSInteger dayRow = [self selectedRowInComponent:0];
- NSInteger hourRow = [self selectedRowInComponent:1];
- NSInteger minuteRow = [self selectedRowInComponent:2];
- NSInteger daySeconds = dayRow * 24 * 60 * 60;
- NSInteger hourSeconds = hourRow * 60 * 60;
- NSInteger minuteSeconds = minuteRow * 5 * 60;
- return (daySeconds + hourSeconds + minuteSeconds);
- }
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:
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:
- # lib/widgets/widget.rb
- # Superclass for our form element "widgets"
- class Widget
- include Merb::GlobalHelpers
- # This is necessary to include Merb::GlobalHelpers
- class_inheritable_accessor :_default_builder
- # These are for convenience
- attr_accessor :params
- attr_accessor :session
- def initialize(params = {}, session = {})
- self.params = params
- self.session = session
- end
- def value
- end
- end
- # lib/widgets/weight_input_switcher.rb
- class WeightInputSwitcher < Widget
- attr_accessor :attribute_name, :weight_in_kg, :unit_preference
- # View methods
- def setup(attribute_name, weight_in_kg = nil, unit_preference = session[:unit_preference])
- self.attribute_name = attribute_name
- self.weight_in_kg = weight_in_kg
- self.unit_preference = unit_preference
- self
- end
- def switcher
- html = "<div class='weight_input_switcher'>"
- html += weight_input("lbs")
- html += weight_input("kg")
- html += weight_input("st")
- html += "</div>"
- html
- end
- def weight_input
- html = "<span class='weight_input #{unit_preference}'>"
- html += weight_input_field
- html += "</span>"
- end
- def weight_input_field
- if unit_preference == "st"
- stone, lbs = if weight_in_kg.to_i == 0
- ["",""]
- else
- weight_input_value(weight_in_kg, unit_preference).split(" st ")
- end
- html = text_field attribute_name, :value => stone, :name => "weight_input[stone]", :class => "weight_input_field stone"
- html += "<span class='weight_unit'>stone</span>"
- html += text_field attribute_name, :value => lbs, :name => "weight_input[lbs]", :class => "weight_input_field stone lbs"
- html += "<span class='weight_unit'>lbs</span>"
- else
- value = weight_input_value(weight_in_kg, unit_preference)
- html = text_field attribute_name, :value => value, :name => "weight_input", :class => "weight_input_field"
- html += "<span class='weight_unit'>" + unit_preference.to_s + "</span>"
- end
- html
- end
- # Parse methods
- # We kinda cheat here and use params. Need to figure out a better way to do this.
- def value
- if params["weight_input"]["stone"]
- "#{params["weight_input"]["stone"]} st #{params["weight_input"]["lbs"]} lbs"
- else
- "#{params["weight_input"]} #{Units.weight_full_to_abbreviation_map[unit_preference]}"
- end
- end
- private
- def weight_input_value(weight_in_kg, unit_preference)
- begin
- weight_in_kg.to_display_weight_without_unit(unit_preference)
- rescue
- ""
- end
- end
- end
- # The following is added to global_helpers.rb
- def widget(klass, *args)
- klass.new(params, session).setup(*args)
- end
- # An example of using the "Widget" to display form elements
- widget(WeightInputSwitcher, :weight, @current_user.current_weight).switcher
- # An example of using the "Widget" to get the value of form elements
- weight_widget = widget(WeightInputSwitcher, :weight, 0, "", user[:unit_preference])
- 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!




Cool!
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.
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.