Over the weekend, in building a super-fun, and very simple one page app called Am I Ruby? with Kate, Rachel, and Sophie, we stumbled upon a most curious feature of Rails.
Embracing object oriented design to the best of our abilities, we attempted to implement the so-called “fat mode, skinny controller” principle. Simply, most of the application specific logic should be in a model and the controller should serve only as a traffic cop routing information to and from views.
Seeing as this was a simple one page and no immediate need for persistence, our search
model was a tableless. In other words, it was a plain old Ruby class.
After much of our application was built, we set up our controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
The controller would define a new Search
model in order to serve up an object to the form_for @search
in our main page’s view.
1 2 3 4 |
|
We were feeling pretty good…until we saw this: undefined method 'model_name' for #<Search:0x007fbc18f959d0>
. OUCH!! Especially since we never even wrote a method model_name
…what gives?
A quick glance at the trace shows us this:
Actionpack, activeview, activesupport…a lot of things I kind of recognize but am not super familiar about. About halfway down this huge trace I finally see one I do know about for sure: activerecord
. Since our Search
class was a tableless class, we never told it to inherit from ActiveRecord. So at some point, form_for
tried to call model_name
on a class that doesn’t have access to that method. Let’s assume that has something to do with ActiveRecord.
Model Name
As it turns out, rails form helpers rely heavily on ActiveRecord. It’s not magic: at some point rails has to introspect on the object and be able to know what controller and action to send the submitted data and how to format the params hash. That’s really it.
model_name
is a part of the ActiveModel module that helps rails with its naming conventions, magically pluralizing and singularizing our model names at will. How do we fix this? We have to give our class access to this method. We search for it and find where it lives in the rails source code.
1 2 3 |
|
Refresh the page and we get a new error: undefined method 'to_key' for #<Search:0x007fbc1849fcb0>
. to_key
is also an activemodel method but not in the Name
class, its in the Conversion
class. What it does is return an array of all the object’s attributes as keys. We’re going to assume that those will later be used in form_for
’s creation of the params hash.
1 2 3 4 |
|
Refresh again, and we get another error: undefined method 'persisted?'
. We know the drill by now: persisted?
is an activerecord method that returns a boolean that tells us whether or not an object has been persisted to the database. At this point we have a couple of options.
We can define persisted?
and basically be done. As long as the Search
class has a defined attribute for every field in the form, the page will load error free.
1 2 3 4 5 6 7 8 9 10 |
|
That’s not too bad. But I have an inquiring mind and want to keep pushing this further.
We can keep going down the rabbit hole of including/extending modules based on the error messages we find.
1 2 3 4 5 6 7 8 9 |
|
But since I’ve now added 5 modules, 3 of which are a part of ActiveRecord and its well after midnight and there’s no end in sight, I’m starting to get really tired of this. I have this feeling that all roads will at some point lead to ActiveRecord::Base
.
When we look at the activerecord source code, we find that require 'active_model'
is literally the third line. In fact, Base
has no code of its own - it just requires, includes, and extends nearly all the other Active
modules in the Rails source code. So why not just make the class inherit from ActiveRecord and cover all our (ActiveRecord) bases?
1 2 |
|
So even though our model is not going to talk to a database, we can still give it all the functionality of ‘tabled’ class that allows form_for
to do its thing. But, if you’re afraid this might confuse another developer (or future you), this is all you need to add to your class to allow it to interact with form_for
:
1 2 3 4 5 6 7 8 9 10 |
|