Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement custom url helpers and polymorphic mapping #23138

Merged
merged 16 commits into from Feb 21, 2017
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -1,3 +1,48 @@
* Prefer `remove_method` over `undef_method` when reloading routes

When `undef_method` is used it prevents access to other implementations of that
url helper in the ancestor chain so use `remove_method` instead to restores access.

*Andrew White*

* Add the `resolve` method to the routing DSL

This new method allows customization of the polymorphic mapping of models:

``` ruby
resource :basket
direct(class: "Basket") { [:basket] }
```

This comment has been minimized.

Copy link
@jrochkind

jrochkind Feb 25, 2017

Contributor

Is this a typo, it says "Add the resolve" method, but then the example uses direct not resolve. Not sure if the example is valid as it is and is demonstrating direct, or if it's a typo and the example is demonstrating resolve. Trying to figure out these new features.

This comment has been minimized.

Copy link
@georgeclaghorn

georgeclaghorn Feb 25, 2017

Member

It’s corrected in master.

This comment has been minimized.

Copy link
@pixeltrix

pixeltrix Feb 25, 2017

Author Member

It was almost fixed in master - it is now 😄


``` erb
<%= form_for @basket do |form| %>
<!-- basket form -->
<% end %>
```

This generates the correct singular URL for the form instead of the default
resources member url, e.g. `/basket` vs. `/basket/:id`.

Fixes #1769.

*Andrew White*

* Add the `direct` method to the routing DSL

This new method allows creation of custom url helpers, e.g:

``` ruby
direct(:apple) { "http://www.apple.com" }
>> apple_url
=> "http://www.apple.com"
```

This has the advantage of being available everywhere url helpers are available
unlike custom url helpers defined in helper modules, etc.

*Andrew White*

* Add `ActionDispatch::SystemTestCase` to Action Pack

Adds Capybara integration directly into Rails through Action Pack!
@@ -2020,6 +2020,111 @@ def concerns(*args)
end
end

module CustomUrls
# Define custom url helpers that will be added to the application's
# routes. This allows you override and/or replace the default behavior
# of routing helpers, e.g:
#
# direct :homepage do
# "http://www.rubyonrails.org"
# end
#
# direct :commentable do |model|
# [ model, anchor: model.dom_id ]
# end
#
# direct :main do
# { controller: 'pages', action: 'index', subdomain: 'www' }
# end
#
# The return value from the block passed to `direct` must be a valid set of
# arguments for `url_for` which will actually build the url string. This can
# be one of the following:
#
# * A string, which is treated as a generated url
# * A hash, e.g. { controller: 'pages', action: 'index' }
# * An array, which is passed to `polymorphic_url`
# * An Active Model instance
# * An Active Model class
#
# NOTE: Other url helpers can be called in the block but be careful not to invoke
# your custom url helper again otherwise it will result in a stack overflow error
#
# You can also specify default options that will be passed through to
# your url helper definition, e.g:
#
# direct :browse, page: 1, size: 10 do |options|
# [ :products, options.merge(params.permit(:page, :size)) ]
# end
#
# NOTE: The `direct` methodn can't be used inside of a scope block such as
# `namespace` or `scope` and will raise an error if it detects that it is.
def direct(name, options = {}, &block)
unless @scope.root?
raise RuntimeError, "The direct method can't be used inside a routes scope block"
end

@set.add_url_helper(name, options, &block)
end

# Define custom polymorphic mappings of models to urls. This alters the
# behavior of `polymorphic_url` and consequently the behavior of
# `link_to` and `form_for` when passed a model instance, e.g:
#
# resource :basket
#
# resolve "Basket" do
# [:basket]
# end
#
# This will now generate '/basket' when a `Basket` instance is passed to
# `link_to` or `form_for` instead of the standard '/baskets/:id'.
#
# NOTE: This custom behavior only applies to simple polymorphic urls where
# a single model instance is passed and not more complicated forms, e.g:
#
# # config/routes.rb
# resource :profile
# namespace :admin do
# resources :users
# end
#
# resolve("User") { [:profile] }
#
# # app/views/application/_menu.html.erb
# link_to 'Profile', @current_user
# link_to 'Profile', [:admin, @current_user]
#
# The first `link_to` will generate '/profile' but the second will generate
# the standard polymorphic url of '/admin/users/1'.
#
# You can pass options to a polymorphic mapping - the arity for the block
# needs to be two as the instance is passed as the first argument, e.g:
#
# direct class: 'Basket', anchor: 'items' do |basket, options|
# [:basket, options]
# end
#
# This generates the url '/basket#items' because when the last item in an
# array passed to `polymorphic_url` is a hash then it's treated as options
# to the url helper that gets called.
#
# NOTE: The `resolve` methodn can't be used inside of a scope block such as
# `namespace` or `scope` and will raise an error if it detects that it is.
def resolve(*args, &block)
unless @scope.root?
raise RuntimeError, "The resolve method can't be used inside a routes scope block"
end

options = args.extract_options!
args = args.flatten(1)

args.each do |klass|
@set.add_polymorphic_mapping(klass, options, &block)
end
end
end

class Scope # :nodoc:
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
@@ -2040,6 +2145,14 @@ def nested?
scope_level == :nested
end

def null?
@hash.nil? && @parent.nil?
end

def root?
@parent.null?
end

def resources?
scope_level == :resources
end
@@ -2113,6 +2226,7 @@ def initialize(set) #:nodoc:
include Scoping
include Concerns
include Resources
include CustomUrls
end
end
end
@@ -103,6 +103,10 @@ def polymorphic_url(record_or_hash_or_array, options = {})
return polymorphic_url record, options
end

if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options])
end

opts = options.dup
action = opts.delete :action
type = opts.delete(:routing_type) || :url
@@ -123,6 +127,10 @@ def polymorphic_path(record_or_hash_or_array, options = {})
return polymorphic_path record, options
end

if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options], only_path: true)
end

opts = options.dup
action = opts.delete :action
type = :path
@@ -156,6 +164,14 @@ def polymorphic_path_for_action(action, record_or_hash, options)
polymorphic_path(record_or_hash, options.merge(action: action))
end

def polymorphic_mapping(record)
if record.respond_to?(:to_model)
_routes.polymorphic_mappings[record.to_model.model_name.name]
else
_routes.polymorphic_mappings[record.class.name]
end
end

class HelperMethodBuilder # :nodoc:
CACHE = { "path" => {}, "url" => {} }

@@ -255,9 +271,13 @@ def handle_model(record)
[named_route, args]
end

def handle_model_call(target, model)
method, args = handle_model model
target.send(method, *args)
def handle_model_call(target, record)
if mapping = polymorphic_mapping(target, record)
mapping.call(target, [record], only_path: suffix == "path")
else
method, args = handle_model(record)
target.send(method, *args)
end
end

def handle_list(list)
@@ -303,6 +323,14 @@ def handle_list(list)

private

def polymorphic_mapping(target, record)
if record.respond_to?(:to_model)
target._routes.polymorphic_mappings[record.to_model.model_name.name]
else
target._routes.polymorphic_mappings[record.class.name]
end
end

def get_method_for_class(klass)
name = @key_strategy.call klass.model_name
get_method_for_string name
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.