The intent of this article is to approach development and debugging of Ruby on Rails applications from the perspetive of a C++ developer. In Part I I discussed some of the fundamental differences between Ruby on Rails and C++ development.
I started a simple "blog with comments" example to step through and showed how to use the ruby console to debug the model and what some of the exception messages returned to the view are telling us.
Where are the exceptions?
While we know the model is right (we debugged it already) sometimes there is no exception information being displayed. Where do we turn then? In this part of the article I am going to expand on our example and go deeper into what to look for there are no meaningful exception messages.
At this point in our example, we have a model for Posts and we can create Comment instances for a Post. Let’s try to see where are view stands by going to http://localhost:3000/posts. You should have one post from the steps of our previous article so click on "Show" to see that post.
We get this:
The error tells us exactly where the issue is (at our <%= render @post.comments %>
call) and what the problem is: it can’t find the partial view for comments/comment. So, let’s remedy that by creating a simple partial view at app/views/comments/_comment.html.rb:
<p>
[<%= comment.username %></td>] - <%= comment.body %>
</p>
<p>---</p>
This is about as simple as a view can be. We are just showing the username and body of the text.
Refresh our browser and bam…we see our new comment in the comments section.
Adding AJAX
Now that we can show comments, let’s add the ability to add comments. In the post’s ‘show’ view (app/views/posts/show.html.erb), after our comments div, add the following:
<%= form_tag :action => :add_comment, :remote => true do %>
<p>
Name: <%= text_field_tag :username %> <br/>
Comment: <%= text_area_tag :body %> <br/>
<%= submit_tag "Add Comment", :id => @post.id %>
</p>
<% end %>
While this isn’t very complex, it has one little bit in it that is a little Rails magic: :remote => true. This specifies that this call is going to be a remote AJAX call to the post controller’s "add_comment" action. We’ll be passing in the username and body as the parameters and id for the id of the post (so we know which post to add the comment to). Let’s create the add_comment action.
In app/controllers/posts_controller.rb add the add_comment method:
def add_comment
@post = Post.find(params[:id])
@comment = @post.comments.create(:username => params[:name], :body => params[:body])
respond_with @comment do |format|
format.js
end
end
We already know that the post.comments.create
will work because we used the same method in the console earlier. The next few lines (the respond_with
block) tells rails to return with the script add_comment.js
(more of that Rails convention magic), passing in the newly created comment.
That means we will need comment.js
somewhere. It’s a script so it goes in the app/assets/javascripts/add_comment.js, right? Go ahead and create that file and just put the one line in there:
$("#comments").append("<%= escape_javascript render @comment %>");
A very simple jQuery call that adds our new comment to the end of the "comments" div. Users should be able to enter a comment and see the comments updating without leaving the page.
- Experienced Rails developers probably see all the mistakes I’ve made here and are laughing hysterically at my noob-ness. Joke’s on you; I did it on purpose so I could have something to fix (that’s my story and I’m sticking to it).
Let’s test it out by refreshing our post view in the browser:
Curses. We’ve added a new action but it isn’t one of the standard controller actions so we need to tell rails that it exists. Open config/routes.rb. You can see there is already a line for "resources :posts". We need to modify that line to specify our add_comment action:
resources :posts do
resources :comments
member do
post 'add_comment'
end
end
Now refresh our post view in the browser.
Awesome! Now we are showing comments and allowing the user to enter some. So, let’s do it. Enter a name & comment and press "Add Comment".
Well, that was anti-climatic. At least the error message tells you exactly what you need to do. We never told the controller that it could return a script. Add the following line at the top of the post controller class (app/controllers/posts_controller.rb):
respond_to :json, :only => :add_comment
This tells rails that the add_comment can return javascript results.
Go back to the post page and refresh. Look at that! The comment we just entered is there. So, we know it went into the add_comment
action of the post controller and created the comment (like we knew it would). Now we just have to see if our new respond_to line fixed it to return the script.
Enter yet another comment and click "Add Comment".
Blank. Nothing. We can go back, refresh the post, enter another comment. All of it ends in blankness. Nothing for me to even take a screenshot of to show you how broken it is.
The server log
Since we know the request is getting sent and received by the action (the comment was created), we need to figure out what Rails is doing when the action tries to respond.
In a terminal, at your application’s folder, enter:
$ tail -f log/development.log
This will print out any information about requests coming into the rails server and any responses it makes. Try going to the post page and add another comment. What is in the log?
These looks a little cryptic but tells us everything about the request. The actual url coming in is /posts/2/add_comment?remote=true and is being processed by the controller PostsController#add_comment.
There are a couple of things here that are a little off. First, it is being processed HTML, when we wanted JavaScript. Secondly, (and this is very subtle), we don’t want remote=true as a parameter; that was supposed to tell Rails to handle it as an AJAX call from the view, not passed into our action.
Well that lets us know that something is wrong with how we have the remote call setup in the form. Taking another look at the post show view (app/views/posts/show.html.erb), the line we care about is:
<%= form_tag :action => :add_comment, :remote => true do %>
There isn’t a lot there to change. This seems to be well formed so it could be bug in Rails (never!) Try grouping the parameters like this so it is clear they are all being supplied to form_tag as arguments:
<%= form_tag({ :action => :add_comment}, {:remote => true }) do %>
Go back to the post page in the browser and try to add another comment.
Well? Um. Did it do anything?
Go check the development.log you are still tailing.
Now it looks right. Processing is PostsController#add_comment as JS. That’s right. Looking further down, we see exactly what’s going on with the missing template. It can’t find out script. By searching through that message we can tell that we don’t have our script in the right place.
Move the script from app/assets/javascripts/add_comment.js to app/views/post/add_comment.js.erb (note the extension change). Refresh the post page in the browser and try again.
Success!
Now we have our goal: we can enter posts, view the posts, and add comments that dynamically appear in the window.
Last thoughts
There are a few debugging methods I didn’t cover here. First, if you’re a C++ developer, you’re looking for breakpoints. Rails has a few options for breaking into and stepping through code. You can use one of the existing IDEs (my personal favorite being RubyMine). Another option is to use a gem like Pry to add breakpoints and investigate what’s going on. Both of these will provide C++-like source-level debugging.
Another omission, while very important, is debugging client side scripts. My personal approach to debugging client-side scripts is to use the Chrome Developer Tools. In addition to these tools, a rails-specific Chrome extension, the Rails Panel, is available.
That’s it. Have fun riding the rails. (groan)