ruby-on-railsrubynestedpolymorphic-associationsnested-routes

Rails link_to polymorphic parent, which can have a nested route


I have the Comment model, which is polymorphic associated to commentable models like Project, User, Update etc. And I have a page where a user can see every User's comment. I want a link near each comment with an address of an object this comment is associated with. I could write something like that:

link_to 'show on page', Object.const_get(c.commentable_type).find(c.commentable_id)

But this will work only for not nested routes (like User). Here's how my routes look like:

resources :users do
  resources :projects, only: [:show, :edit, :update, :destroy]
end

So when I need a link to a Project page, I will get an error, because I need a link like user_project_path. How can I make Rails to generate a proper link? Somehow I have to find out if this object's route is nested or not and find a parent route for nested ones


Solution

  • You could use a bit of polymophic routing magic.

    module CommentsHelper
      def path_to_commentable(commentable)
        resources = [commentable]
        resources.unshift(commentable.parent) if commentable.respond_to?(:parent)
        polymorphic_path(resources) 
      end
    
      def link_to_commentable(commentable)
         link_to(
           "Show # {commentable.class.model_name.human}",
           path_to_commentable(commentable)
         )
      end
    end
    
    class Project < ActiveRecord::Base
      # ...
      def parent
        user
      end
    end
    
    link_to_commentable(c.commentable)
    

    But it feels dirty. Your model should not be aware of routing concerns.

    But a better way to solve this may be to de-nest the routes.

    Unless a resource is purely nested and does not make sense outside its parent context it is often better to employ a minimum of nesting and consider that resources may have different representations.

    /users/:id/projects may show the projects belonging to a user. While /projects would display all the projects in the app.

    Since each project has a unique identifier on its own we can route the individual routes without nesting:

    GET /projects/:id - projects#show
    PATCH /projects/:id - projects#update
    DELETE /projects/:id - projects#destroy
    

    This lets us use polymorphic routing without any knowledge of the "parent" resource and ofter leads to better API design.

    Consider this example:

    Rails.application.routes.draw do
      # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
    
      resources :projects
    
      resources :users do
        # will route to User::ProjectsController#index
        resources :projects, module: 'user', only: [:index]
      end
    end
    
    class ProjectsController < ApplicationController
      def index
        @projects = Project.all
      end
      # show, edit, etc
    end
    
    class User::ProjectsController < ApplicationController
      def index
        @user = User.joins(:projects).find(params[:user_id])
        @projects = @user.comments
      end
    end
    

    This would let us link to any project from a comment by:

    link_to 'show on page', c.commentable
    

    And any users projects by:

    link_to "#{@user.name}'s projects", polymorphic_path(@user, :projects)