faithfulgeek.org

Agile web development

How to allow users to enter a dollar sign in money fields on a form using Rails

January 16th, 2008

Long title, but well worth the article:

[Update (3/27/2008): I have released this code as a plugin. Please see my entry announcing the acts_as_currency plugin.]

I’m still newbieish to Ruby on Rails. I recently had a form in which I had a field that took a monetary amount (let’s say, hourly rate). In the database (and therefore in ActiveRecord), we want to store that as a decimal type so we can perform calculations on it. However, on the front end we want the user to be able to enter $55. Unfortunately, this is not completely straightforward. Since ActiveRecord sees the type as a decimal, it will rip out the ’$’ and set anything after it to zero (0). Therefore, $55 becomes 0. That is no good! I played around with a few different ideas, trying to modify the validation of the attribute. While it seemed a good idea at the time, it did not work; this is because at the time we start validating, the salary attribute has already been cast to a decimal and we lost our data. After that I tried using a before_validation callback (thanks to Nate Klaiber for the idea), but that caused problems with my rspec test, because when I set the hourly_pay attribute it doesn’t revalidate, therefore the before_validation callback never fires.

meh.

Then I decided to consult my good friend, Google. I came across this gem from one of my tweeps, Joe O’Brien. After reading that, I was able to write:


    def hourly_pay=(hourly_pay)
        hourly_pay.gsub!(/^\$/, '') if hourly_pay =~ /^\$/
        write_attribute(:hourly_pay, hourly_pay)
    end

I reran my rspec tests, and all passed! Yay! However, I could have more currency fields in the future. Not to mention, this was a huge pain in the ass for me to figure out, and I want to save everyone else the hassle that I went through. Therefore, I used a little metaprogramming to create an acts_as_currency method for the model class:


1    class ActiveRecord::Base
2    
3           def self.acts_as_currency(*args)
4
5            args.each do |arg|
6                define_method arg.to_s + '=' do |currency_string|
7                    currency_string.gsub!(/^\$/, '') if currency_string =~ /^\$/
8                  write_attribute(arg, currency_string)
9             end
10                
11               end
12
13      end
14
15  end

Here is the line-by-line explanation, due to popular demand:

So line 1 redefines the ActiveRecord::Base class from which all ActiveRecord objects inherit.

Line 3 defines the acts_as_currency method as an instance method on the ActiveRecord::Base class. *args allows multiple arguments to be passed in a comma-delimited format.

Line 5 Loops through each argument, running the code inside do…end, with arg being the current argument in the loop.

Line 6 This is the real magic! define_method allows you to dynamically define a new method on whatever class you are currently working with. Here we use arg.to_s + '=', so in the case of our acts_as_currency :hourly_pay we are defining hourly_pay=(currency_string), which is a custom setter for the hourly_pay attribute. If we add more symbols ala acts_as_currency :hourly_pay, :tax_withheld then we’ll get more setters defined (tax_withheld=(currency_string)).

Line 7 This is the body of the method you are defining. In this case, we check if currency_string starts with $, and if so, strip it out.

Line 8 We then write this new value to the attribute already defined in the ActiveRecord model.

That’s it! Six (you read that right: 6! [that’s for you, Corso]) lines of code is all it took to add an innumerable amount of currency attributes to any ActiveRecord model. If you think you’ll have a need for it, download the code here (right-click | Save Link As…) and drop in the ‘lib’ folder of your Rails project. Eventually I may make a plugin out of it. Please note: I am not responsible for the behavior of this code in your environment; it is provided merely to help you if you can use it; if you have questions I will try to answer, but no promises :).

Anyway, with that out of the way, enjoy! If you have any suggestions, questions, whatever on this code, please comment!

Leave a Reply