8

I needed to add a simple change to foo_path, so I did this:

module ApplicationHelper
  # [...]

  def foo_path(foo, options = {})
    options.merge!(bar: foo.some_attribute)
    super
  end

  # [...]
end

Now, this works when called from a view, but when called from a controller, the original variant without my additions is used.

How can I override the according helpers (_path/_url) application wide?

user569825
  • 2,369
  • 1
  • 25
  • 45

4 Answers4

15

I think the cleanest way to achieve that is to customize routes.rb file (at least for static default parameters). Docs: http://guides.rubyonrails.org/routing.html#customizing-resourceful-routes

Default param example:

get "/foo(/:bar)" => "my_controller#index", defaults: { bar: "my_default" }

Default param value example + scope:

scope '/(:rec_type)', defaults: { rec_type: 'mammo' }, rec_type: /mammo|face/ do
  resources :patients
end

Other options (for dynamic restriccions):

Advanced routing constraints:

If you need more advanced/dynamic restrictions, take a look to this guide: http://guides.rubyonrails.org/routing.html#advanced-constraints.

Override default_url_options:

Also, you can override default_url_options method to automatically add some attributes (params) using routes helpers: http://guides.rubyonrails.org/action_controller_overview.html#default-url-options.

You can set global default parameters for URL generation by defining a method called default_url_options in your controller. Such a method must return a hash with the desired defaults, whose keys must be symbols:

class ApplicationController < ActionController::Base
  def default_url_options(options = {})
    if action_name == 'foo' # or other conditions
      options[:bar] = 'your_defaults' # add here your default attributes
    end

    options
  end
end

Override to_param:

Rails routing system calls to_param on models to get a value for the :id placeholder. ActiveRecord::Base#to_param returns the id of a model, but you can redefine that method in your models. For example, given:

class Product < ActiveRecord::Base
  def to_param
    "#{id}-#{title}"
  end
end

This would generate: /products/254-Foo

markets
  • 6,709
  • 1
  • 38
  • 48
  • I updated the relevant line to clarify that actually a dynamic value is merged into `options` (`foo.some_attribute`). – user569825 Feb 23 '15 at 13:59
4

In Rails 3 and 4, route url helpers all resides in this module Rails.application.routes.url_helpers. So we can include a module to override methods inside it.

module FooUrlHelper
  def foo_path(foo, options = {})
    options.merge!(bar: foo.some_attribute)
    super
  end
end

# Works at Rails 4.2.1
Rails.application.routes.url_helpers.send(:include, FooUrlHelper)

# For Rails 4.2.6, I thought the following worked, but there seems to be issues, don't have time to figure out a solution yet
Rails.application.routes.named_routes.url_helpers_module.send(:include, FooUrlHelper)

You can just place this inside initializers

I tried this in console and controller and seems to work fine.

lulalala
  • 17,572
  • 15
  • 110
  • 169
  • The advantage of this method is that, unlike adding or aliasing methods in ApplicationController, it works in all places that use URL helpers such as mailer templates. – Robin Daugherty Jun 19 '16 at 04:24
  • Can you explain what exactly you put in an initializer? Did you place the `module FooUrlHelper` in the `/helpers/` directory, and then made an initializer containing the single line `Rails.application.routes.named_routes...`? – DelPiero Apr 19 '21 at 16:50
  • @DelPiero I created a file under initializers, containing the module definition and one "include" call, so it is included at boot time. – lulalala Apr 20 '21 at 01:29
3

Looks like you are facing wrong way. You would better add :bar param in your route, like:

get :foo, :bar => "bar"

Otherwise, please provide more descriptive details about your problem.

Edit.

Here is the solution(basic on your post update):

class ApplicationController < ActionController::Base
  def foo_path(foo, options = {})
    options.merge!(bar: foo.some_attribute)
    super
  end  
  helper_method :foo_path
end
intale
  • 831
  • 5
  • 7
  • I updated the relevant line to clarify that actually a dynamic value is merged into `options` (`foo.some_attribute`). – user569825 Feb 23 '15 at 13:59
  • It will not work that way. That param must be present in the url, otherwise how would rails know, what value of :bar param should be. So, mainly, the answer is `foo_path(foo, :bar => foo.some_attribute)` which will generate url something like `/foo/:id?bar=some_attr_value` – intale Feb 23 '15 at 14:06
  • It already does work - just that it's not used when called from controllers. It "knows" because it's explicitly assigned the value of `foo.some_attribute`. – user569825 Feb 23 '15 at 14:10
  • Looks like misunderstanding. I meant that :bar param must be present in the url. So, basically, you just want `foo_path` to always add :bar param when called without explicit passing it? – intale Feb 23 '15 at 14:14
  • Yes - Instead of `foo_path(@foo, bar: @foo.some_attribute)` I want to just call `foo_path(@foo)`. **:bar** is part of the defined route (something like `get ':id/:bar' ...`). – user569825 Feb 23 '15 at 14:18
  • Thanks @intale. It works good in production, but the helper is still ignored when called from **test** environment. Therefore I am also looking for a way to tackle this on a lower level (see http://stackoverflow.com/q/28305050/569825 if interested). – user569825 Feb 24 '15 at 19:49
2

As the current answers show, you are not overriding a rails helper (as defined somewhere in the app/helpers folder) but a route helper (as defined in config/routes.rb).

Calling the method foo_path from a view works, because first Rails will look through all available application helper methods before looking through the available routing helper methods.

Calling the method from a controller does not work, because Rails will only go through the available routing helper methods, not through the routing helpers.

If you want to call an application helper from a controller, use this inside your controller:

view_context.foo_path(...)

This requires you to prepend view_context. for every call to foo_path inside every controller, so it is not a perfect solution but it should work.

zwippie
  • 15,050
  • 3
  • 39
  • 54
  • 1
    +1 for the explanation. Thanks. Maybe this should say "not through the rails helpers" instead? "[...] will only go through the available routing helper methods, not through the routing helpers. [...]" – user569825 Feb 23 '15 at 15:08