24

I want to use rails' familiar helpers, but with slightly altered functionality. The way I see it, I want to be able to do something like:

module AwesomeHelper
  #... create alias of stylesheet_link_tag to old_stylesheet_link_tag
  def stylesheet_link_tag(*args)
    if @be_awesome
      awesome_stylesheet_link_tag *args
    else
      old_stylesheet_link_tag *args
    end
  end
end

The way I see it, I have three options:

  1. Monkey patching: Reopening the rails helper module. If the rails team ever change the name of their helper module, my code becomes a source of brittleness. Not insurmountable, but not ideal.
  2. Use different method names: Trying to stick to the common rails interface may be my downfall. My changes may become a source of confusion for other developers
  3. Detaching methods (new): Not sure whether this would work, or whether it would have the same drawbacks as 1. Will research this, but this might be a good starting point.

So the question here is, am I stuck with one of these sub-optimal solutions, or is there another way that I haven't considered? If I go for option 3, is there a way to do it without directly addressing the rails helper module?

(Note: I have removed the context, as it adds nothing to the question.)

user208769
  • 2,216
  • 1
  • 18
  • 27

4 Answers4

39

There's a better way than any of your listed options. Just use super:

module AwesomeHelper
  def stylesheet_link_tag(*sources)
    if @be_awesome
      awesome_stylesheet_link_tag *sources
    else
      super
    end
  end
end

Overriding stylesheet_link_tag in AwesomeHelper will ensure that, when stylesheet_link_tag gets invoked, Ruby will encounter it in the method lookup path before it hits ActionView::Helpers::AssetTagHelper. If @be_awesome is true, you get to take charge and stop things right there, and if not, the call to super without parentheses will transparently pass through all the arguments up to the Rails implementation. This way you don't have to worry about the Rails core team moving things around on you!

Cade
  • 3,151
  • 18
  • 22
  • You know what... that is so crazy obvious, I'm trying to rack my brains to work out why I thought it wouldn't work! I'm gonna try it tonight and, if it works, I will be having severe words with my brain, probably involving a brick wall. After, of course, accepting your answer... :D – user208769 May 10 '12 at 15:34
  • 1
    @user208769 Hehehe. That's awesome. As best I understand, overriding methods in this manner is generally preferable in any circumstance. [Class#ancestors](http://ruby-doc.org/core-1.9.3/Module.html#method-i-ancestors) can be really helpful in figuring out a good place in the method lookup path to hijack method dispatching (or where your custom module with the override needs to be included to best effect). – Cade May 10 '12 at 15:44
  • What? :) You have to be kidding! This is a HUGE gotcha! Your way you have to include you helper in EVERY class that includes AssetTagHelper. Time flies and you or someone else can forget about needing your patch to be included. You just include AssetTagHelper and start to wonder: why my site now looks differently? It's good when you and patch-maker are the same person. But what if not? – jdoe May 16 '12 at 10:01
  • 2
    @jdoe Yes, you are correct. In order to make use of one's code, it needs to be included. :) I don't consider that a "gotcha". Using [AbstractController::Helpers::ClassMethods::helper](http://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper) in `ApplicationController` may help in mitigating your concern. – Cade May 16 '12 at 13:23
6

Try using alias_method:

module AwesomeHelper
  alias_method :original_stylesheet_link_tag, :stylesheet_link_tag

  def stylesheet_link_tag(*sources)
    if @be_awesome
      awesome_stylesheet_link_tag *sources
    else
      original_stylesheet_link_tag *sources
    end
  end
end
oldhomemovie
  • 14,621
  • 13
  • 64
  • 99
5

I don't use this gem, so I'll answer you in a more generic way.

Let's say you want to log calls to link_to helper (yeah, contrived example, but shows the idea). Looking in API gives you understanding that link_to located inside the ActionView::Helpers::UrlHelper module. So, you create some file in your, say, config/initializers directory with the following contents:

# like in config/initializers/link_to_log.rb
module ActionView::Helpers::UrlHelper

    def link_to_with_log(*args, &block)
        logger.info '**** LINK_TO CALL ***'
        link_to_without_log(*args, &block) # calling the original helper
    end

    alias_method_chain :link_to, :log
end

The core of this functionality -- alias_method_chain (clickable). Use it AFTER defining the method xxx_with_feature.

jdoe
  • 15,665
  • 2
  • 46
  • 48
  • Yeah, this approach was what I meant by "monkey patching specific rails modules" - which works well, but if rails core change their module names, my code breaks. This may not be a big deal, but I'm curious to see if there are other solutions. That said, had forgotten about alias_method_chain, thank you for reminding me of that! – user208769 May 08 '12 at 09:32
  • P.S: Have updated the question to remove the gem example. Hopefully this layout is less confusing! Thank you. – user208769 May 08 '12 at 09:51
  • Risk is always there! If you worry about `alias_method_chain` then you shouldn't: it exists since version 1.4.0 (year 2007). If you worry about other parts of your program then ensure decent tests coverage. – jdoe May 08 '12 at 09:55
  • 1
    Deprecated. http://stackoverflow.com/questions/3689736/rails-3-alias-method-chain-still-used – Vadorequest May 14 '14 at 12:58
2

I would really encourage you to consider your option #2, overriding the behavior of rails methods in a way that is obvious to the caller.

Your new method should be called awesome_stylesheet_link_tag so that other Rails devs can read your code and ask the question "What is so awesome about the link tag?".

As a smaller change you could do the override but pass in :awesome => true as an argument so they at least have a clue that something is afoot.

Changing the behavior of a widely used method like stylesheet_link_tag creates a potential future misunderstanding where none is needed.

austinfromboston
  • 3,791
  • 25
  • 25
  • Thanks for the input. While normally I agree, in this particular case I think the consistency is justified - I'm doing this to use wicked_pdf and have exactly the same code generate a PDF or a web page. While wicked_pdf by default does as you say (wicked_pdf_stylesheet_link_tag), it requires too much repetition for me, and I think it's acceptable to expect that the functionality might change if you are generating a PDF. But you make a good point, and some handy tips, so thank you. – user208769 Jun 02 '12 at 14:32