DRY Up Your Url Helpers

April 18, 2007

This tutorial shows you how to simplify url generation in combination with RESTful resources by extending the url_for helper. This approach will also work with nested routes and other helpers like form_tag and link_to.

One of the concepts of REST is: each resource has its own unique URI. We will enhance the url_for helper to generate this unique URI for an arbitrary record.

Example Models

First we need a simple example. We have 2 models: User and Article. For our url generation to work we have to add following code to our models:

  1. class User
  2. has_many :articles
  3. def to_params
  4. {:id => permalink}
  5. end
  6. end
  7. class Article
  8. belongs_to :user
  9. def to_params
  10. {:user_id => user.permalink, :id => permalink}
  11. end
  12. end

This is necessary for nested routes to play nicely with our url generation code. We are now able to find the parameters for each record to generate an unique URL.

Pretty URLs

In a previous post I demonstrated the use of meaningful urls. We are going now the same way.

Users are identified by an URL like:

/users/matthias-georgi

Each user may write articles, which are located at:

/users/matthias-georgi/articles

If I want to write a new article, I will use this URL:

/users/matthias-georgi/articles/new

Editing an existing article would end up on this URL:

/users/matthias-georgi/articles/my-first-post;edit

For nested resources to get working you define in config/routes.rb:

  1. map.resources :users do |user|
  2. user.resources :articles
  3. end

The traditional way to generate urls is to call the resource helpers:

  1. article_url(article.user, article)

This is redundant, as the article already knows its user.

The Resource Helper

Add following module into your lib folder and include the module in both your application controller and application helper. The most important bit is the url_for method. It will automatically generate the right url for your resource.

  1. module ResourceHelper
  2. def plural_class_name(record)
  3. singular_class_name(record).pluralize
  4. end
  5. def singular_class_name(record)
  6. record.class.name.underscore.tr('/', '_')
  7. end
  8. def params_for(record)
  9. if record.respond_to?(:to_params)
  10. record.to_params
  11. else
  12. {:id => record.to_param}
  13. end
  14. end
  15. def collection_url(collection, record, options)
  16. if record
  17. params = params_for(record)
  18. params["#{singular_class_name(record)}_id".to_sym] = params.delete(:id)
  19. url_for options.merge(params).merge(:controller => collection)
  20. else
  21. url_for options.merge(:controller => collection)
  22. end
  23. end
  24. def member_url(record, options)
  25. url_for options.merge(params_for(record)).merge(:controller => plural_class_name(record))
  26. end
  27. def url_for(*args)
  28. if [String, Hash].any? {|type| args.first.is_a? type }
  29. super(*args)
  30. else
  31. if args[0].is_a?(Symbol)
  32. collection_url(args[0], args[1], :action => 'index')
  33. else
  34. member_url(args.first, :action => 'show')
  35. end
  36. end
  37. end
  38. def new_url_for(collection, record=nil)
  39. collection_url(collection, record, :action => 'new')
  40. end
  41. def edit_url_for(record)
  42. member_url(record, :action => 'edit')
  43. end
  44. def path_for(*args)
  45. if args[0].is_a?(Symbol)
  46. collection_url(args[0], args[1], :action => 'index', :only_path => true)
  47. else
  48. member_url(args.first, :action => 'show', :only_path => true)
  49. end
  50. end
  51. def new_path_for(collection, record=nil)
  52. collection_url(collection, record, :action => 'new', :only_path => true)
  53. end
  54. def edit_path_for(record)
  55. member_url(record, :action => 'edit', :only_path => true)
  56. end
  57. end

Usage

So how can you use this stuff actually?

It is pretty easy: just pass the record instead of the url hash and the unique url will be generated automatically.

The url of a collection is treated differently. You have to pass the name of the collection, which is the controller name. For nested resources you have to pass additionally the record, the collection is belonging to.

Some examples:

  1. new_path_for(:users) # => '/users/new'
  2. path_for(user) # => '/users/harald'
  3. edit_path_for(user) # => '/users/harald;edit'
  4. path_for(:articles, user) # => '/users/harald/articles'
  5. new_path_for(:articles, user) # => '/users/harald/articles/new'
  6. path_for(article) # => '/users/harald/articles/article-1'
  7. edit_path_for(article) # => '/users/harald/articles/article-1;edit'
  8. # This works for helpers like url_for, form_tag or link_to.
  9. link_to article.title, article
  10. form_tag :articles
  11. form_tag article, :method => 'put'

If anybody is interested I will release this stuff as plugin. I think other helpers could benefit as well as you can pass your records around and each helper may generate the appropriate url.


Posted in category Ruby by Matthias Georgi. Tagged with helpers.
Similar Posts