I'm creating a view helper in Rails in which I need to use the existing form_with
helper from ActionView::Helpers::FormHelper
. I've tried to call it using ActionView::Helpers::FormHelper.for_for
which doesn't work because it's an instance method, so I need to include it.
I've found two ways which both seem wrong.
First: include the Rails module in my module.
module ApplicationHelper
include ActionView::Helpers::FormHelper
def helper_method
form_for ...
end
end
Coming from languages like Python/Kotlin, I don't like that this pollutes the interface of my ApplicationHelper
with the methods of FormHelper
when it's a module I'm using only internally for the implementation of my method.
Second: include the foreign module inside my method.
module ApplicationHelper
def helper_method
include ActionView::Helpers::FormHelper
form_for ...
end
end
This works for a single method, but seems bad from a maintenance perspective -- if I create a bunch of helper methods using form_for
, they would all need to include the module separately. Also, I think that this means the module is included each time the method gets called, so that makes it slower.
What I'm trying to achieve is to do a sort of "private include" - use FormHelper::form_for
without exposing it to the interface of my module. (I would like to call it explicitly with the module name - FormHelper::form_for
- but I can live with just form_for
.) Is there a canonical way to do this in Ruby, or should I get used to it and just include the module?
(I've also come to this problem when trying to use the Rails' route helpers in a class that wasn't a controller.)
I'm creating a view helper in Rails in which I need to use the existing form_with
helper from ActionView::Helpers::FormHelper
. I've tried to call it using ActionView::Helpers::FormHelper.for_for
which doesn't work because it's an instance method, so I need to include it.
I've found two ways which both seem wrong.
First: include the Rails module in my module.
module ApplicationHelper
include ActionView::Helpers::FormHelper
def helper_method
form_for ...
end
end
Coming from languages like Python/Kotlin, I don't like that this pollutes the interface of my ApplicationHelper
with the methods of FormHelper
when it's a module I'm using only internally for the implementation of my method.
Second: include the foreign module inside my method.
module ApplicationHelper
def helper_method
include ActionView::Helpers::FormHelper
form_for ...
end
end
This works for a single method, but seems bad from a maintenance perspective -- if I create a bunch of helper methods using form_for
, they would all need to include the module separately. Also, I think that this means the module is included each time the method gets called, so that makes it slower.
What I'm trying to achieve is to do a sort of "private include" - use FormHelper::form_for
without exposing it to the interface of my module. (I would like to call it explicitly with the module name - FormHelper::form_for
- but I can live with just form_for
.) Is there a canonical way to do this in Ruby, or should I get used to it and just include the module?
(I've also come to this problem when trying to use the Rails' route helpers in a class that wasn't a controller.)
Share Improve this question edited Feb 5 at 10:24 jirka asked Feb 5 at 10:02 jirkajirka 311 silver badge7 bronze badges 2 |1 Answer
Reset to default 5You do not need / want to include ActionView::Helpers::FormHelper
.
In Ruby you do not need to include another module to gain access to it's methods if both are being included in the same class. You can just assume that someone else is bringing it to the party:
module A
def method_from_a
"Hello world"
end
end
module B
def method_from_b
method_from_a
end
end
class Foo
include A
include B
def bar
method_from_b
end
end
puts Foo.new.bar # "Hello World"
While you could include A in B the end result is the same.
Since we know that all the helpers get mixed into the glorious mess that is the view context you can just assume that it and all the other Rails view helpers are going to be there.
module MyHelper
def wrapper_method(...)
form_for(...)
end
end
What I'm trying to achieve is to do a sort of "private include" - use FormHelper::form_for without exposing it to the interface of my module.
This doesn't actually exist in Ruby. Include copies the methods of module to the recipient but they retain whatever visibility they where declared with. Ruby doesn't have a use x,y from Z
package system type functionality so we just do it differently.
Coming from languages like Python/Kotlin, I don't like that this pollutes the interface of my ApplicationHelper with the methods of FormHelper when it's a module I'm using only internally for the implementation of my method.
It's not a language thing. It's a Rails thing. Many Rubyists frown just as heavily on the idea of mushing together all your modules and hoping you get the right one.
While you're right to worry your concern is misplaced as it all gets smooshed together anyways. Separating up modules into different modules/files is really just window dressing when they all get included in the same object in the end. I'm not sure how good of an interface you should expect from a junk drawer.
Remember that Rails is built for rapid prototyping and has a pretty heavy focus on "just works". Helpers are easy and convient and arguably better than having the complexity in the view but not uncontroversial.
You can easily also completely opt of writing helpers. The most common alternatives are variants of the decorator/presenter pattern.
If you want to add specific functionality to a form and use good old classical inheritance you can instead create a custom form builder class. Like this example I shamelessly stole from the docs that wraps a radio button in a <div>
:
class MyFormBuilder < ActionView::Helpers::FormBuilder
def div_radio_button(method, tag_value, options = {})
@template.content_tag(:div,
@template.radio_button(
@object_name, method, tag_value, objectify_options(options)
)
)
end
end
<%= form_with model: @person, builder: MyFormBuilder do |f| %>
I am a child: <%= f.div_radio_button(:admin, "child") %>
I am an adult: <%= f.div_radio_button(:admin, "adult") %>
<% end -%>
Here the view helpers are called on the view context (@template) which is passed in though the constructor. You can also configure Rails to use your form builder by default.
It's also recommended that you use the newer form_with
method instead of the old form_for
method which has a lot of quirks and a wonky pre-keywords signature.
NoMethodError
because you're callinginclude
from an instance method. But even for class methods, it would merely defer theinclude
call until you invoke the method. It won't limit the inclusion to the method's scope. – Stefan Commented Feb 5 at 11:12