Update 2014-06-08: This post is over two years old now. Although I’ve heard the below walkthrough works mostly as expected, I’ve been away from Rails too long to know the ins and outs of the current version of Rails and all the gems used. So a word of warning: I can’t guarantee all of the below will work line-for-line anymore. Feel free to ping me on Twitter if you find any changes.

I’ve been working on and off with Rails for a few years now, but when I started I had little HTML/CSS/JS knowledge from which to build. Most of my web experience I learned along the way in the context of Rails.

HTML and CSS were much easier to build familiarity with than JavaScript. I always found more time-tested best practices concerning HTML and CSS than I did with JS/AJAX. AJAX with Rails techniques seemed to have changed significantly between releases of Rails major (and even minor) versions.

I am by no means an expert, but my goal with this post is to walk beginners through a working technique for vanilla AJAX comments on resources based on Rails 3.2.x.

What we’re making

Our goal is to make a comment form that we can attach to any resource in our Rails app. It will look something like this:

Our goal
Our goal

The layout is pretty standard. A create form sits on top of a list of comments (newest first).

Our example resource throughout this post is an “Event”. We’ll only discuss it in terms of being an example generic resource with comments that belong to it.

Create

When a logged in user enters a comment and clicks “Create Comment”, the browser sends a message back to the server with the comment body, the resource name, and resource id. Once the server processes the message, it will send the comment body rendered in HTML in the same partial as the other comments were rendered with.

In the meantime, on the client side, we’ll be doing some basic jQuery effects to let the user know their comment is being processed. We’ll disable the textarea and submit button so they don’t accidentally submit the same comment twice.

Once the server returns our new HTML, we’ll reenable the form controls, clear the text from the textarea, and then add the new HTML to the top of the comment list.

To keep it simple for now, we won’t be handling the error cases in significant detail.

Processing order

  • route: GET /event/1
  • controller: events#show
  • view: events/show.html.haml
  • partial: comments/_form.html.haml
  • partial: comments/_comment.html.haml
  • user: add comment body and click create
  • js: comments.js.coffee -> ajax:beforeSend
  • route: POST /comments
  • controller: comments#create
  • partial: comments/_comment.html.haml
  • js: comments.js.coffee -> ajax:success

We’ll touch on each of these steps, but not necessarily in that order.

Delete

We’ll also allow users to delete comments (eventually only comments they’ve created!). When they click the ‘x’ next to the comment, we’ll prompt them with a standard confirmation. If they answer yes, we’ll then send the comment id to the server.

On the browser side, we’ll immediately dim the comment to half opacity to let the user know we’re trying to delete the comment. Once we receive a response indicating the comment has been removed from the database, we’ll then hide the comment in their browser the rest of the way.

There are a few error conditions we should handle here as well, but we won’t look at those in this post.

Processing order

  • route: GET /event/1
  • controller: events#show
  • view: events/show.html.haml
  • partial: comments/_form.html.haml
  • partial: comments/_comment.html.haml
  • user: click “x” next to comment
  • user: click “yes” to confirm
  • js: comments.js.coffee -> ajax:beforeSend
  • route: DELETE /comments/1
  • controller: comments#destroy
  • partial: comment.json
  • js: comments.js.coffee -> ajax:success

The first half is the same as we’ll see for comment creation, so we’ll focus on the last half mostly in that order.

Where to start?

First place to start is getting our backend comment system in place. We’ll be using the acts_as_commentable_with_threading gem (although we won’t be using the threading right away).

The instructions for setting this up are pretty simple. I’m just using ActiveRecord and SQLite right now.

  • Put the gem in your bundle gem acts_as_commentable_with_threading.
  • Run bundle install.
  • Run the migrations rails g acts_as_commentable_with_threading_migration.
  • Run rake db:migrate.
  • Add acts_as_commentable to the Event model class (and any other model you want to have comments).

      # event.rb
      class Event < ActiveRecord::Base
    		acts_as_commentable
      end		
    

This post is supposed to be more about AJAX than Rails associations, but it’s worth mentioning that acts_as_commentable uses a polymorphic association. This means that any of your models can reference the same kind of comment model object, and we don’t have to have a separate table in our database for an EventComment or a VideoComment for example. Each comment record keeps track of what type of object its parent is, which will be important later since we need to know information about the parent in order to create a comment.

Routes

Next we’ll set up our routes just to get that out of the way. We’re going to let the CommentsController handle creation and deletion of comments, so the routes should point there.

	# routes.rb
	resources :comments, :only => [:create, :destroy]

This will give us two methods from the following urls (from rake routes).

	$ rake routes
	comments 	POST 		/comments(.:format) 	comments#create
	comment 	DELETE 	/comments/:id(.:format) 	comments#destroy

This is going to give us a commments_path helper and comment_path(:id) helper to complete our POST and DELETE requests, respectively. It will forward requests to those URLs to the CommentsController’s create and destroy methods. The create method has no parameters in the URL string. The destroy method takes the comment’s id as the single parameter of the URL string. Like we mentioned earlier, in order to create the comment, we’ll need a few more parameters. We’ll talk more about that when we get to the form.

Alternate implementation

Aside: An alternate implementation worth mentioning is to include comments as a nested resource beneath each resource that has them. It would look something like this:

	# routes.rb - alternate
	resources :events
		resources :comments, :only => [:create, :destroy]
	end
	
	resources :videos
		resources :comments, :only => [:create, :destroy]
	end

This works fine if your resources are all siblings. In my case, I have Video nested within Event already. It gets pretty hairy pretty quickly and gives you (unnecessarily) complicated routes and URLs. In this case, we’ll go with the other implementation that includes the necessary data about the comment’s parent in the HTTP POST data rather than the URL string.

Again, it works either way so always tailor your implementation based on your particular situation.

show.html.haml

Now that we’ve got the bridge between the view and the controller built (the route), we’ll tackle the show view template.

Our goal is to be able put the comment “block” (the add new form and the list of previously created comments) anywhere. In this example, we’ll stick it in the show view of the EventsController.

(Sidebar: I use Haml and simple form, sorry in advance to users of other templates. Hopefully you can still follow along.)

	/ show.html.haml
	.event
		%h1= @event.name
	.comments
		%h2 Comments
		= render :partial => 'comments/form', :locals => { :comment => @new_comment }
		= render :partial => 'comments/comment', :collection => @comments, :as => :comment

As you can see, our show template expects 3 different instance variables from the EventsController.

  • @event: the unique Event object we’re showing.
  • @new_comment: a new Comment object that acts as our framework for building out the comment form. It exists only in Rails for now and has not been created in the database yet.
  • @comments: an array of all or just a subset of the Comment objects that exist as children of the @event object (in reverse chronological order of course).

In our views/comments folder, we have the two partials _form.html.haml and _comment.html.haml. _form expects a local variable named comment as an input to help build the new comment form. comment.html.haml is our partial for displaying a single comment. It takes a collection of comments and tells the renderer to treat each object in the collection as a comment.

events#show

Before we dig into writing each partial, let’s step backwards in the chain of events and go back to our EventsController to set up those instances variables that the show template will be looking for.

	# events_controller.rb
	class EventsController < ApplicationController
	  def show
	    @event = Event.find(params[:id])
	    @comments = @event.comment_threads.order('created_at desc')
	    @new_comment = Comment.build_from(@event, current_user.id, "")
	  end
	end

The first line of the show method should be par for the course. We’re pulling the event in question from the database based on the id provided in the URL. Rails automatically inserts a render 'show' for us at the end of the method.

The second line looks a little fishy. We’re using a helper method included in acts_as_commentable_with_threading to get the comments associated with the @event and order them by date. You might also want to do pagination at this step too, but with our nested event->comment architecture, it might also warrant an AJAX solution to load more (that’s a topic for another post).

The third line creates a placeholder comment object that acts as sort of a carrier for our parent object info. This new blank comment object will carry with it a reference to the parent @event and therefore its object type and id, and the current user. The build_from method is another helper created by acts_as_commentable_with_threading.

comments/_form.html.haml

Now we can continue on to our new comment form partial.

	# _form.html.haml
	.comment-form
	  = simple_form_for comment, :remote => true do |f|
	    = f.input :body, :input_html => { :rows => "2" }, :label => false
	    = f.input :commentable_id, :as => :hidden, :value => comment.commentable_id
	    = f.input :commentable_type, :as => :hidden, :value => comment.commentable_type
	    = f.button :submit, :class => "btn btn-primary", :disable_with => "Submitting…"

Let’s step through line by line.

First, we’ll wrap the form with the comment-form class.

Next, we’re going to use simple form to create a form block for our comment. Adding :remote => true will provide the Rails magic to turn our standard form into an AJAX one. The form_for helper is smart enough in this case to pick the correct URL and HTTP method. We could specify it directly as:

	= simple_form_for comment, :url => comment_path, :method => 'post', :remote => true do |f|

The first input is the textarea for our comment body. Nothing special here, just limiting the rows to 2 and turning the label off.

The next two inputs are hidden from the user and will be included with the form submission to the server. We’re including the commentable_type or class name of the parent object and its id so that our CommentsController will know what object to link the new comment to.

Aside: I want to mention that since these hidden inputs are technically open to alteration, they must be properly sanitized by the server before being acted upon. By altering these values, the user could potentially create a new comment on a different object type and/or an object they aren’t allowed to see.

Our last form element is a submit button with Twitter Bootstrap classes for styling. Clicking this will trigger the AJAX action and submit our form data to the CommentsController for handling. The disable_with takes care of some of the JS we’d have to write by disabling the submit button.

I’m going to skip the JS for now and move onto the CommentsController implementation. We’ll get back to the JS in a moment.

CommentsController

If you recall earlier, we set up routes to our CommentsController for two methods: create and destroy. Let’s take a look at create.

	# comments_controller.rb
	class CommentsController < ApplicationController
	  def create
	    @comment_hash = params[:comment]
	    @obj = @comment_hash[:commentable_type].constantize.find(@comment_hash[:commentable_id])
	    # Not implemented: check to see whether the user has permission to create a comment on this object
	    @comment = Comment.build_from(@obj, current_user.id, @comment_hash[:body])
	    if @comment.save
	      render :partial => "comments/comment", :locals => { :comment => @comment }, :layout => false, :status => :created
	    else
	      render :js => "alert('error saving comment');"
	    end
	  end
	end

The first thing we do is grab a reference to the form data. Our form data is in the params hash under the :comment symbol. We’ll store it as @comment_hash for use below.

Next we need to derive the parent object where the comment was created. Luckily, we included the commentable_type and commentable_id in our form data. @comment_hash[:commentable_type] will return the string "Event". We can’t call find on a string, so we have to turn it into a symbol that Ruby recognizes. We can use constantize to do this conversion (it would be a good idea at this point to check to make sure the commentable_type is legitimate). With a fully qualified Event class we can call the class method find and pass it the :commentable_id. Out pops our event object.

The next step is to determine whether the current_user has permission to create the comment on the object. This depends on your authentication system, but should definitely be included.

We now have references to all the objects we need in order to create the comment. We’ll use the build_from helper method again and give it the object, current_user, and the body of the comment.

We need to save the comment back to the database. If the save is successful, we’re going to do a few things.

  • Render the single comment partial with our new comment as the local variable. This will give the comment all the markup it needs to be inserted directly into the existing page.
  • :layout => false will tell the renderer not to include all the extra header and footer markup.
  • :status => :created returns the HTTP status code 201 as is proper.

If the save is not successful, we need to tell the user that there was a problem. I’m leaving this outside the scope of the post simply because there are several different ways of doing this depending on how you set up your layout. Above, all we’re doing is popping up an alert box to the user. You should consider this an incomplete implementation.

Aside: using Rails to render HTML is a technique opposite that of returning raw JSON and using client-side JS libraries to handle all things view related. You may want to look into something like Ember.js.

JavaScript for create

We’re finally back to the JavaScript, or more specifically, CoffeeScript. I’m not an expert in either, but for this stuff you don’t need to be one. I’m using CoffeeScript because it makes the code slightly cleaner.

The only CoffeeScript we’re going to write can sit comfortably in the asset pipeline in the comments.js.coffee file (more specifically, app/assets/javascripts).

	# comments.js.coffee
	jQuery ->
	  # Create a comment
	  $(".comment-form")
	    .on "ajax:beforeSend", (evt, xhr, settings) ->
	      $(this).find('textarea')
	        .addClass('uneditable-input')
	        .attr('disabled', 'disabled');
	    .on "ajax:success", (evt, data, status, xhr) ->
	      $(this).find('textarea')
	        .removeClass('uneditable-input')
	        .removeAttr('disabled', 'disabled')
	        .val('');
	      $(xhr.responseText).hide().insertAfter($(this)).show('slow')

What is code actually doing? We’re simply registering for callbacks on the AJAX requests that will originate from our comment form. When those events occur, we’re going to run functions.

$(.comment-form) targets the comment-form class we applied to the div that wraps our comment form partial. This allows us to actually use multiple comment forms on a single page if we want to.

.on is the jQuery function that binds an event to a function. It replaces the older jQuery functions .bind, .delegate, and .live. You can read about it here.

The first event we’re binding to is "ajax:beforeSend". When the user clicks the submit button, Rails will trigger this event, and our function will be called. The arguments passed to the function (and all the available callbacks) can be found on the jquery-ujs wiki.

The function that runs on this event is embedded as anonymous. We could call a function that exists elsewhere just as easily.

$(this) is the jQuery object version of the .comment-form div that was involved in the click. Alternatively, we could grab a reference to the form from $(evt.currentTarget). We’ll use $(this) to extract the textarea element in the next line. .find('textarea') will select textarea elements within the form. In our case, we only have one. We then chain two functions together to perform two operations on the textareas.

	$(this).find('textarea')
	  .addClass('uneditable-input')
	  .attr('disabled', 'disabled');

is equivalent to:

	$(this).find('textarea').addClass('uneditable-input');
	$(this).find('textarea').attr('disabled', 'disabled');

addClass adds the uneditable-input class to our textarea, which will perform some Bootstrap styling to our textarea, but not actually make it uneditable.

attr adds the disabled='disabled' element to our textarea actually disabling the user input.

We’re then chaining another .on for the ajax:success event that gets called if the AJAX call returns successfully. Our first move is to find the textarea and undo the temporary disabling (you may want to consider doing this at the ajax:complete event, because it should be done regardless of whether the AJAX was successful). You’ll notice we chained one additional function .val('') at the end. This will clear the textarea in anticipation of the user adding another comment. You wouldn’t want to do that in the error case, because the user should have an opportunity to resubmit the comment without having to retype it.

We’re finally ready to add the nicely formatted comment to the top of our comment feed.

  • $(xhr.responseText) gets a jQuery object version of the response HTML returned by the server.
  • .hide() disappears our new div so it can be animated in.
  • .insertAfter($(this)) places our new div after the comment form. If you want to put it somewhere more specific, you can replace the $(this) selector with a more specific selector.
  • .show('slow') animates our new div sliding down from the form.

_comment.html.haml / deletion

I skipped our single comment template, so I’ll add it here for completeness. This will lead us into the comment deletion section.

	# _comment.html.haml
	%div.comment{ :id => "comment-#{comment.id}" }
	  %hr
	  = link_to "×", comment_path(comment), :method => :delete, :remote => true, :confirm => "Are you sure you want to remove this comment?", :disable_with => "×", :class => 'close'
	  %h4
	    = comment.user.username
	    %small= comment.updated_at
	  %p= comment.body

Our wrapper div has a comment class, and a CSS id unique to each comment. We’re not actually going to use that id, but it could be useful in the future.

link_to should look familiar. Our display text is an x. The link will go to the delete path we created earlier in the Routes section. To refresh your memory, it will go to /comments/:id. :method => :delete tells Rails to use the DELETE HTML method.

:remote => true performs the Rails AJAX magic like we saw earlier with the creation form. :confirm pops up a JS alert to confirm the user wants to do remove the comment. :disable_with makes sure the user can’t try to delete the comment while the server is processing the first request. And the close class is Bootstrap styling.

Another reminder: you’ll probably want to conditionally display the delete link to the comment creator and admins. Draper is a good option for doing this cleanly.

The rest of the markup should be pretty straightforward.

Back to CommentsController

Time to add the destroy method to your CommentsController.

	# comments_controller.rb
	def destroy
	  @comment = Comment.find(params[:id])
	  if @comment.destroy
	    render :json => @comment, :status => :ok
	  else
	    render :js => "alert('error deleting comment');"
	  end
	end

@comment will track down the comment-to-be deleted from the database (check that user is allowed to delete it!).

Then try to destroy the comment. This time, when the call completes successfully, I’m sending raw json back to the client with an ok status. There are a myriad of options here. Use what’s best for your app.

And on error I’m copping out again and sending back JS.

Aside: if you want to do some informal testing, I recommend throwing a sleep 5 call before the if statement so you have more time to observe your AJAX.

JavaScript for destroy

Back to our comments.js.coffee file.

	jQuery ->
	  # Create a comment
	  # ...
	
	  # Delete a comment
	  $(document)
	    .on "ajax:beforeSend", ".comment", ->
	      $(this).fadeTo('fast', 0.5)
	    .on "ajax:success", ".comment", ->
	      $(this).hide('fast')
	    .on "ajax:error", ".comment", ->
	      $(this).fadeTo('fast', 1)

We’re going to use the other incarnation of .on for the reason I’ll explain in a moment. This time we’re calling .on on the whole DOM. We specify our event first as we did before, but now we’ll add the ".comment" selector as the second argument. Again, this applies to all of our comment divs with the comment class.

We’re not going to bother including the arguments to the ajax event callbacks (for example (evt, xhr, settings)); we don’t need them.

$(this) refers to the comment div that generated the event. We’re going fade the entire comment to half opacity before sending the request to the server by calling .fadeTo('fast', 0.5). On success, we’ll animate the comment fading the rest of the way out and disappearing to show the user the request was completed succesfully. On error, we’ll fade the comment back to full opacity to show that the comment still exists.

The reason we used $(document) this time instead of calling .on on the selector directly is because it will apply the callback to newly created DOM elements as well. For example, I can add a comment and then immediately delete it without refreshing the page.

Wrap up

This turned out to be quite the mega-post. I may have gone into too much detail, but I’m hoping this has enlightened any new Rails devs out there.

We didn’t actually write that much JavaScript, and most of it was simply for decoration. But this should give you the building blocks you need to add more interesting functionality on AJAX triggers. I highly recommend this jQuery reference/overview.

Discuss this on Hacker News.