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!
- Posted 7 months ago
- comments[0]
- Permalink
- Digg This!