Making a Rails Helper predictable

Rails helpers are pretty messy. Everyone who’s ever tried to simply include, say, the UrlHelper into an object did instantly crash with some NoMethodError either complaining about

  • a missing method that should be included in the helper already, but isn’t
  • an undefined method for nil:NilClass
target = Object.new
target.extend ActionView::Helpers::UrlHelper
target.url_for('/fix/me')
 
#=> url_helper.rb:94:in `url_for': undefined method `escape_once' for #<Object:0xb7496300> (NoMethodError)

That’s due to dependencies between different helpers and the instances they’re mixed into.

Wrapping the UrlHelper

I’m gonna explain how ActiveHelper can help solving this problem by wrapping the UrlHelper step-by-step.

This will make the UrlHelper a predictable, well-behaving class. Helpers should be good kids, nothin’ more.

Here’s a first version for the new helper.

module Rails
  class UrlHelper < ActiveHelper::Base
    include ActionView::Helpers::UrlHelper
 
    provides :url_for, :link_to, :button_to # add more if you need.
  end
end

We derive the new helper from ActiveHelper::Base, include the original module and define our interface with provides.

A rails scenario setup

In Rails, this is what roughly happens when a view is rendered.

controller = ActionController::Base.new
view       = ActionView::Base.new([], {}, controller)
 
view.extend ActiveHelper
view.use Rails::UrlHelper

Usually the use call would happen in the controller’s action processing, or in #render.

view.url_for('fix/me')
# => url_helper.rb:85:in `send': undefined method `url_for' for nil:NilClass (NoMethodError)

Ok, send is accessing nil. Let’s inspect the original helper.

  def url_for(options = {})
    ...
    @controller.send(:url_for, options)

The rails helpers blindly accesses an instance var @controller, it simply assumes the instance it is mixed into does provide it.
Even worse, it uses send to invoke a method on the controller. That’s one of rails major drawbacks and must be fixed.

Providing the @controller

Unfortunately, we won’t touch the original UrlHelper and open pandora’s box.

Remember, we got a helper instance which mixed-in the original UrlHelper module. That means we’d just have to provide the @controller instance variable in our helper instance.

module Rails
  class UrlHelper < ActiveHelper::Base
    include ActionView::Helpers::UrlHelper
 
    provides :url_for, :link_to, :button_to # add more if you need.
 
    def initialize(*args)
      super(*args)
      @controller = parent.controller
    end

We setup an ivar @controller by accessing parent, that’s the object which is using us, so it’s the ActionView and that itself does provide a public accessor to its controller.

Fixing another helper dependency

Not nice, but anyway, cleaner than things were before.

view.url_for('fix/me')
url_helper.rb:94:in `url_for': undefined method `escape_once' for #<Rails::UrlHelper:0xb747b794> (NoMethodError)

Oh. #escape_once is needed in rails’ original helper, too. I will spare the details about the code now, the method is defined in TagHelper. However it is not mixed in UrlHelper. Shitty helpers.

That’s another broken dependency we’ll have to fix.

We now have three possibilities

  • we could wrap TagHelper in an ActiveHelper as well, and derive UrlHelper
  • wrapping and use‘ing it inside UrlHelper would be the second solution
  • or simply include the original module, hoping all will go well

For now I’ll choose the third, anyway, that’s far from good architecture.

module Rails
  class UrlHelper < ActiveHelper::Base
    include ActionView::Helpers::UrlHelper
    include ActionView::Helpers::TagHelper
 
    provides :url_for, :link_to, :button_to # add more if you need.
 
    def initialize(*args)
      super(*args)
      @controller = parent.controller
    end

Let’s see if it works.

view.url_for('fix/me') #=> "fix/me"
view.button_for('fix/me')
#=> url_helper.rb:298:in `button_to': undefined method `protect_against_forgery?' for #<Rails::UrlHelper:0xb74a7eac> (NoMethodError)

Now what’s that again? Looks as if #button_for needs protect_against_forgery?.

Delegating dependency to the ActionView

Let’s express that dependency.

module Rails
  class UrlHelper < ActiveHelper::Base
    include ActionView::Helpers::UrlHelper
    include ActionView::Helpers::TagHelper
 
    provides :url_for, :link_to, :button_to # add more if you need.
 
    needs :protect_against_forgery?
    ...

Having that defined, calls to protect_against_forgery? will be delegated back to the view. That should work.

view.button_for("Click me!", 'fix/me')
#=> <form method="post" action="fix/me" class="button-to"><div><input type="submit" value="Click me!" /></div></form>

Yeah!

Where to go from here?

Now that you know how easy it is to clean up the Rails helpers (or Merb, or …!!!), go and do it. I’d love to see ActiveHelper being used to replace nasty modules and bringing back the power of OOP to your views.

One Response to “Making a Rails Helper predictable”

  1. Johan Hernandez Says:

    Interesting approach, thanks for sharing.

Leave a Reply