Skip to content

Embedded Nested Polymorphic Comments

odigity edited this page Sep 13, 2010 · 1 revision

Preface

As a newbie working on my first Rails app, I looked for tutorials, snippets, or screencasts that could help me implement what I thought was a very commonplace design pattern – a list of comments with an Add Comment form attached to the bottom of several models’ show pages (like articles and podcast episodes). I found lots of references that covered some of the related concepts (including make_resourceful), but none that implemented exactly what I describe above. So I had to do a bit of guess-work, which is overwhelming for a beginner already struggling with getting up to speed on Ruby, ActiveRecord, routing, controllers, views, REST, etc.

I managed to get it working, and want to post the recipe I cobbled together to help others on a similar journey reach their destination quicker. The make_resourceful Google group seems to be dead, so I’m posting it on this wiki. I hope more experienced Rails developers will come by and review this, as it is highly improbable that a beginner would produce a best-practice solution to this problem.

Purpose

Let’s review our design requirements.

Model

There are three models: Article, Podcast, and Comment. We want both Article and Podcast to be have a one-to-many relationship with Comment so that many users can post many comments on all article and podcast pages. We want to do this polymorphically (meaning any comment record may point to either a article or a podcast) rather than create two separate comment tables, one each for article and podcast.

Controller

Let’s review the situation, action-by-action:

  • index: We don’t need this, since the list of comments will always be in the context of the parent model – the list of comments belonging to a specific article or podcast. (The Rails term for this concept is ‘scope’.) This will be implemented by a partial template, not a controller action.
  • show: We don’t need this, since comments will only need to be viewed in list form. We won’t have individual pages that just show the specific comment.
  • new: We don’t need this, since the “Add Comment” form is going to be embedded in the show page for the parent model (article or podcast).
  • create: We do need this, as the “Add Comment” form will need to post the comment to some URL.
  • edit: To keep things simple, we won’t both implementing the ability to edit comments, only create and destroy them. This is a fairly common limitation, and perfectly acceptable.
  • update: Likewise, we won’t need this.
  • destroy: We will need this, as admins will want the ability to delete offensive comments (mostly spam).

So, we only need two routes and two actions: create and destroy. Furthermore, we’ll want them nested so that the context of the parent model is available to the create action without passing it as a hidden field in the form (ugly). In other words, our routes will look like this:

/:parent_model/:parent_model_id/comments
/:parent_model/:parent_model_id/comments/:comment_id/destroy

Example:

/articles/12/comments
/articles/12/comments/57/destroy

(Technically, destroy doesn’t need to be nested since comment_id alone is enough to umabiguously delete the comment, but why implement a half-and-half design?)

View

To keep things DRY, we want to implement a partial template that will display the list of comments for the context object (article or podcast) along with a form to add comments. We can then render that partial from the show page of the two parent models. The partial will need…

  • …to know the context object in order to display it’s comments
  • ..to generate the URLs for the context-appropriate nested routes for the destroy action for each comment (you would only display these links to an admin, of course)
  • ..to generate the URL for the context-appropriate nested route for the create action, for the “Add Comment” form

So, the three relevant templates are:

  • /app/views/articles/show.html.erb
  • /app/views/podcasts/show.html.erb
  • /app/views/shared/_comments.html.erb

Foundation (Journey)

If you’re new, first get up to speed on the basic concepts and the examples that already exist. Let’s walk through the core concepts.

Nested

A resource who’s routes are nested below the routes of the parent resource. If the parent resource was Article, and comments were nested below article, then comment routes would look like /articles/12/comments and /articles/12/comments/57/edit. References:

Railscast 139 describes a very simple version of our scenario. He has Article and Comment models, implements a basic one-to-many relationship, and demonstrates how to nest the routes, so you can see the comments for article 12 at /articles/12/comments.

(Note: The term ‘Nested’ can also refer to nesting a form for one model inside another, so that you could update, say, a Doctor and the names of his Patients all in one giant form. That’s not the kind of nesting talked about on this page – though if you’re interested, check out Railscasts 196 and 197)

Embedded

The term I’m using to describe the fact that I’m putting the index and new functionality directly into the show page of the parent object, rather than creating separate routes, controller actions, and templates.

Polymorphic

A relationship who’s participants are determined dynamically instead of statically. In ActiveRecord, this is implemented by using a compound foreign key (like parent_id and parent_type) instead of a single-field foreign key (like article_id) that can only refer to one table. References:

Railscast 154 describes almost exactly the same scenario as ours – polymorphic nested comments – but creates a full set of routes, actions, and templates for the comments rather than embedding them. (The implementation details and side-effects of embedding are different and challenging enough to be an obstacle to a novice like myself.) He writes a short function (the “secret sauce”) that helps determine which model that parent object belongs to (Article or Podcast, in our case). It’s great, but I have an aversion to dropping fancy bits of code like that into my projects. I like to have the fancy stuff neatly tucked away in well-named and preferably well maintained plugins. :)

Revelation

I had been searching a long time and was despairing of finding that one tool or reference that would finally get me within mental-jumping distance of my goal… until I found make_resourceful. It rocks. If you don’t know about it – if you got here from a web search – go check it out right now. This is the wiki for it, afterall. Just scroll up and click on Source.

Since I’m already pimping Railscast so hard, might as well be consistent:

make_resourceful is great for all projects, but let’s focus on how it specifically helps us close the gap. It already includes the same functionality that the “secret sauce” function from Railscast 154 added – the ability to disambiguate polymorphic parent contexts. In fact, it has enough built-in intelligence to automatically provide sensible default logic for all actions, including polymorphic relationships in combination with nested routes. In fact, the code to implement @make_resourceful@__ ’s defaults, including nested routes and polymorphic relationships, is less than the generic scaffold code produced by Rails for the simpler, general case__.

make_resourceful handles all the logic for you, and also provides you with the proper abstractions to use to implement changes to the logic when you need to. We’ll cover those in the next section.

Implementation

Let’s walk through the implementation from the bottom of the stack upwards.

Routing

/config/routes.rb

map.resources :articles, :podcasts do |item|
  item.resources :comments, :only => [:create, :destroy]
end

The first line sets up the routes for our Article and Podcast resources, and the second line (inside the block) sets up nested routes for the Comment resource beneath the parent resources, but only for the create and destroy actions.

When implementing a polymorphic relationship in ActiveRecord, you have to give a name to the abstract model that Comment is linking to, which is then dynamically resolved at runtime to either Article or Podcast on the fly. A common convention is to use the name “commentable”, but I don’t like using that as a noun, so I chose the generic name “item”. In this context, articles and podcasts are items, and items have comments.

Models

/app/models/article.rb

has_many :comments, :as => :item

/app/models/podcast.rb

has_many :comments, :as => :item

/app/models/comment.rb

belongs_to :item, :polymorphic => true

Standard way of declaring associations in ActiveRecord models.

Controllers

/app/controllers/articles_controller.erb
/app/controllers/podcasts_controller.rb

make_resourceful do
  actions :all
end

That’s literally all we need for the article and podcast controllers. Note that those three lines replace the dozens of lines that the scaffolding provided which explicitly defined methods for all seven REST actions.

/app/controllers/comments_controller.rb

make_resourceful do
  belongs_to :article, :podcast
  actions :create, :destroy
  response_for :create, :create_fails, :destroy, :destroy_fails do
    redirect_to parent_object
  end
end

That’s literally all we need for the comments controller (kind of). (Again, this replaces the generic code.) The belongs_to directive tells make_resourceful that we’re using nested routes (I guess it can’t figure that out automatically yet). The actions directive limits the actions that are handled. The response_for directive let’s us provide custom response logic for a set of actions/events, and requires further explanation.

make_resourceful defines events for each of the seven REST actions which you can hook into with response_for and a few other methods (notably before and after). It also defines :action_fails events for the three modifier actions (create, update, destroy) so you can define different responses in those situations. Now, we need to take a short detour to first review the default logic before we change it to work with our embedded comment design.

Default Logic

The default logic in both the scaffolding code that Rails generates as well as inside make_resouceful is that after the create action is done, you either redirect to the show action if it succeeded or render the new template if it failed (so that the user can see the errors and try again). For example, if a user were trying to add a comment to an article, they would first get to the new page (probably from a link on the article’s show page), which is here:

/articles/12/comments/new

They would then click the submit button, which would POST to:

/articles/12/comments

This would route to the create action. If the create succeeds, it will redirect you to the show page of the newly-created comment:

/articles/12/comments/57

If it fails, it won’t redirect you anywhere, but instead simply render the new template, which would have access to the object that was created but couldn’t be saved. This object contains a record of the error messages and can be displayed by your template. You’ll often see the line <% form.error_messages %> in Rails-generated scaffolding and in code examples which is one way to display them (if you use Formtastic, it does it for you with a flourish).

New Logic

In our scenario, however, there is no comments/new page. The form is embedded in the show page of the parent object (article or podcast). So the user would start here:

/articles/12

They would then click the submit button, which would POST to:

/articles/12/comments

This would route to the create action. If the create succeeds, it will redirect you back to the show page of the parent object which contains both the list of comments and the “Add Comment” form:

/articles/12

If it fails, it would also redirect you back to the show page of the parent object, because we don’t want to write code in our CommentsController that tries to correctly render the show page of the parent object (article or podcast) – that would be insane. You don’t want your code that tightly coupled.

Let’s review the response_for block that was shown above:

response_for :create, :create_fails, :destroy, :destroy_fails do
  redirect_to parent_object
end

Now we can understand what this is for. After all actions, either create or destroy, whether it succeeds or not, the response is to redirect to the parent object. Luckily, make_resourceful provides the aptly named parent_object method to make this easy.

Views

/app/views/articles/show.html.erb

<%= render :partial => "shared/comments", :locals => { :item => @article } %>

/app/views/podcasts/show.html.erb

<%= render :partial => "shared/comments", :locals => { :item => @podcast } %>

In the show template for each of the parent objects, we add a call to render the comments partial template. We pass in a local variable called ‘item’ that we set to the parent object.

/app/views/shared/_comments.html.erb

<% if item.comments.empty? %>
  No comments to display.
<% else %>
  <% for comment in item.comments %>
    <%= comment.body %>
  <% end %>
<% end %>
Add Comment
<% form_for [item, Comment.new()] do |f| %>
  <%= f.input :body %>
  <%= f.submit %>
<% end %>

Here we simply list all comments, if any, then create a form to add new comments. The Rails form_for method accepts an array of ActiveRecord objects which it will use to construct a proper nested URL – in this case, something like:

/articles/12/comments

It knows how to do this because we configured nested routes in the /config/routes.rb file. We create an empty Comment object to pass it simply to let it know the model class to build the route for.

That’s it! We’re done! Well… except for one problem…

Error Handling

The usual way in Rails to handle displaying error messages that result from submitting a form is to rerender the template the user just came from and pass it the object that we failed to save. The object contains a record of the error messages which can then be displayed by the template.

The problem is, we’ve broken that pattern. When the create action in the CommentsController fails, it doesn’t render the article or podcast’s show page, it does a redirect to it… and we lose the object containing our precious error messages.

The fallback I chose was to use the flash instead. (To learn about the flash feature, see the Rails Docs.) So, instead of the combined response_for block we used before, we can instead do this:

response_for :create do
  flash[:success] = "Comment posted."
  redirect_to parent_object
end
response_for :create_fails do
  flash[:error] = current_object.errors.full_messages.to_s
  redirect_to parent_object
end
response_for :destroy do
  flash[:success] = "Comment deleted."
  redirect_to parent_object
end

The success messages are fairly straightforward. For the create_fails event I used the current_object method provided by make_resourceful to access the error messages, convert them to one big string, and stick them in the flash. That way they’ll persist through the browser redirect and be available when the show action is called on the parent object.

You should probably be displaying all flash messages already in your layout template, since this is generally a site-wide feature not specific to our example here. If so, we’re now done.

Epilogue

It’s pretty common to also have your comment model associated with your user model so that if a user is logged in when they post a comment, the comment’s user_id field will get set to the user’s id. Where do we put that code?

make_resourceful provides hooks to run additional bits of code at predefined points in it’s internal action logic. We’ll use one of those. Assuming you have defined two helper methods called current_user and logged_in?, add this bit inside your make_resourceful do block in /app/controllers/comments_controller.rb:

before :create do
  current_object.user = current_user if logged_in?
end