"Add comment", R2.0 templates, and respond_to
Posted 3 months ago by Raul Parolari
Since R2.0 was introduced, several emails pointed out that the ‘add_comment’ (to a blog post) link did not work. They were correct (there was some confusion in the email threads, as most people still continued to use R1.2.3, where the form was working but with minor glitches; so, completely different problems got mixed up).
The reason for the failure with R2.0 was the following; let us glance at the code in the controller was:
def new
@comment = Comment.new
respond_to do |format|
format.html # new.rhtml
format.js # new.rjs (notice this comment!)
end
end
The Respective templates for the two formats were new.rhtml and new.rjs; but the new.rjs template was never executed, and its output discarded by the Ajax client. After an ordeal of tests and (somehow foolish) studies of how the responds_to method works internally in Rails, we went back to read DHH’s note for 2.0, and this section finally caught our attention:
the format of the templates has been separated from its rendering engine. So the new format for templates is action.format.renderer
Could that be the reason for our misery? and if so, what are the ‘format’ and the ‘renderer’ for our Rjs template? a caritative note in a thread clarified it: it had to be .js.rjs (in poetry: the Rjs engine renders JavaScript format..).
So, our derelict new.rjs file was to be born again as new.js.rjs (and then.. clicking for the N+1 time the Add a comment link.. the form suddenly appeared in front of our incredulous eyes!).
All right: given the time spent (the value of N was indeed high) on this affair, let’s at least learn something more.
Extending the example given by DHH with the rjs case, we could
have this correspondence between an edit action and its templates:
def edit
respond_to do |format|
format.html # --> edit.html.erb
format.csv # --> edit.csv.erb
format.js # --> edit.js.rjs
format.iphone # --> edit.iphone.haml
end
end
And of course the templates can be changed (with another ‘renderer’) with no impact on controller logic, which shows the beauty of the design. [Note: new.rhtml still works.. which may be the reason that our brain unconsciously assumed that new.js would still work too!].
Now, a comment on the code itself (read the following only if you are interested in Ruby, beyond Rails). Since I saw a RESTful controller, I have always asked myself this: how in the world the respond_to method works?
Look at the code above: those ‘format.xyz’ calls are method calls, that are going to be executed one after the other (rails magic or not, this is how Ruby works)! but on the other hand we know that the result is like they were in an implicit case statement! so which is it? (if you ventured a timid “perhaps both statements are true?”, you are ahead!).
A detailed explanation of this intriguing mechanism (that we read while trying to debug the problem) would take a few pages; I describe instead a summary, taking as example our add_comment action. First, two preliminary observations:
-
a Rails request can express in the Url a :format parameter (eg: ../posts/3.xml), or specify in the HTTP_ACCEPT header a particular Mime Type requested.
In our case, the “Add comment” link requests an Url like “/blogs/2/posts/3/comments/new” which does not have a :format extension; however Rails’ friend Prototype inserts (in the client) a helpful “text/javascript” in the HTTP_ACCEPT header. - inside the Rails code that we will examine, the “mime type” is not just a string or symbol, but the MIME::Type object which contains all the synonyms (eg, ‘application/javascript’) along with its short string and version. In what follows, I will allude to the ‘mime type’, for simplicity (but you know what it is).
All right: fortified by the above, let’s attack the respond_to method, following our ‘/blogs/2/posts/3/comments/new’ action (invoked via Ajax from the client), whose code we refresh:
def new
@comment = Comment.new
respond_to do |format|
format.html # -> new.html.erb
format.js # -> new.js.rjs
end
end
The steps are the following:
-
The respond_to method starts by retrieving an Array of the Mime types for the request. The first value will be ‘text/javascript’ (from the HTTP_ACCEPT
header), but Rails includes several other choices (at lower priority).
The method also sets up (indirectly) an (empty) hash of @responses. It also receives the block contained in the ‘new’ action (in Ruby, this trick is done via a & prefix to the last parameter listed, like &block). -
Then it starts executing the block: this means that the first ‘format’ call in the block, which is format.html, is executed; this proves what we stated before: those format.xxx statements are indeed executed serially!
But.. how can it work if all of them are executed? well, it all depends on what ‘execution’ means, as we will see next. -
Actually the method ‘format.html’ (or any format.xxx) does not exist; so Ruby invokes the famous method_missing which retrieves the suffix (:html in this case) and creates a Proc object for ‘render :action => @controller.action_name’, ie. ‘render new’.
It also sets (inside this object) 2 variables, ‘template_format’ and ‘content_type’ to ‘html’ to render (when called) the particular template required.
The Proc object is assigned to the @responses hash for the :html mime type (key to hash). -
The same goes for the next call, ‘format.js’; another Proc object is added to the hash @responses for the mime type for ‘js’.
Similarly, it attaches the 2 variables seen above to remember that it needs to render :js.
[Digression: if you wonder how several callable objects can be built with variables that have the same name but different values (and coexist retaining those values until later when called!), good for you; welcome to closures]. -
Now that the list of ‘format.xxx’ calls has ran (storing those ‘render new’ methods in hash @responses) the respond_to method needs to pick one of them; which one will it choose? (dramatic pause..)
Do you remember that in step 1 we built the array @mime_type_priority, and that the first entry listed was the mime type for ‘text/javascript’? well, the method scans the array extracting the mime types looking for a match in the @responses hash; in our case, it finds and calls right away the template ‘new’ for ‘js’, and extract from hash @responses the method with that mime type: that is our new.js.rjs!
Indeed those format.xxx calls listed in the controller action were all executed, serially: but execution meant creating dynamic callable objects (each of which calls a specific template), and then picking up one of them according to a priority list! Ah, Ruby!
Comments
There are 0 comments on this post. Post yours →
Post a comment
Required fields in bold.