Sometimes you are coding a template and you need to refer to the same method chain over and over. For example, you’re coding a template that summarizes activity on recent messages. You iterate through a block of messages, and for each message you want to display some information pertaining to the last comment. You could do it like this:
<div class="active_messages">
<% @active_messages.each do |message| %>
<h1><%= message.title %></h1>
<div class="latest_comment">
<div class="avatar">
<%= avatar_for(message.comments.last.creator) %>
</div>
Latest comment <%= time_ago_in_words(message.comments.last.created_at) %> ago by <%= message.comments.last.creator.full_name %>
</div>
<% end %>
</div>
Everything inside of div.latest_comment deals with the exact same comment, but we have to use a method chain to get the comment each time (message.comments.last).
One solution is to set a local variable inside the iterating block with the knowledge that the variable will be reset after each iteration:
<div class="active_messages">
<% @active_messages.each do |message| %>
<h1><%= message.title %></h1>
<% comment = message.comments.last %>
<div class="latest_comment">
<div class="avatar">
<%= avatar_for(comment.creator) %>
</div>
Latest comment <%= time_ago_in_words(comment.created_at) %> ago by <%= comment.creator.full_name %>
</div>
<% end %>
</div>
One on hand, this is better because the methods called on `comment` are easier to scan. The whole div.latest_comment is more readable without the repeated method chain. On the other hand, setting a local variable is bad style. The local variable assignment creates state without explicitly showing where that state applies. It feels a little too PHP for my taste.
A better approach is to use the `tap` method to scope a variable to a block:
<div class="active_messages">
<% @active_messages.each do |message| %>
<h1><%= message.title %></h1>
<div class="latest_comment">
<% message.comments.last.tap do |comment| %>
<div class="avatar">
<%= avatar_for(comment.creator) %>
</div>
Latest comment <%= time_ago_in_words(comment.created_at) %> ago by <%= comment.creator.full_name %>
<% end %>
</div>
<% end %>
</div>
The `tap` block shows exactly where the scope of the assignment starts and ends. I like how the template explicitly says “now we are going to deal with a comment in the following section, and this is the comment we are working with.”
I just hit on this pattern today while working on a feature and I think it’ll come in handy in the future.
John Holdun
on 08 Feb 10Oh, I like that! Really nice use case for `tap` with good theory to go along with it.
Jeremy Pinnix
on 08 Feb 10Object#tap is available in Ruby 1.9.
Jeremy Pinnix
on 08 Feb 10Apparently Object#tap is also available in Ruby 1.8.7 :-)
Henrik N
on 08 Feb 10Object#tap is in Active Support, so you could use it with Ruby 1.8.6 in a Rails app.
Jeremy Lecour
on 08 Feb 10@Henrik Active Support is not reserved to Rails. You just have to install and require it to have all its features available in your framework of choice, or your own Ruby scripts.
Nate
on 08 Feb 10I find it interesting you don’t do your blocks <- -> to stop the HTML generated being full or whitespace…
RS
on 08 Feb 10@Nate
I don’t care at all what the generated HTML looks like because nobody reads that. It’s important for the source code to be readable, but the generated HTML output is for the browser. It’s not for humans.
Berserk
on 08 Feb 10While I agree that readable source code is vital and more important than the generated HTML I would definitely not go so far as to say that I don’t care at all. Pretty (intended, not white space infested, ...) generated HTML is a sign of attention to detail, IMHO.
Heidmo
on 08 Feb 10I’d choose assign the temp. While tap is clever, it looks busy compared to assigning the temp variable.
Alejandro Moreno
on 08 Feb 10@Berserk, I half-agree.
If you’re developing right in HTML/CSS/Javascript, then certainly, I expect to do “View Source” and be able to make sense of it all.
But if it’s a Rails/PHP/ASP app, it’s all machine-generated HTML, so what’s the point? I think you spend precious “human cycles” trying to make the HTML generator spout pretty code. That’s not part of the generator’s job (unless it’s a CMS!).
DHH
on 08 Feb 10Berserk, I almost ways read the source of a HTML page through a DOM browser anyway that’ll reformat all your code. Not to worry about white space issues.
Jim Jeffers
on 09 Feb 10@dhh @rs Agreed. If you have someone wasting time trying to get the generated HTML output properly aligned… well they’ll be the least productive person on the team won’t they? Just so long as they can properly indent the source code prior to generation. I’ve seen people argue about that in the past.. unbelievable.. :)
Nick Coyne
on 09 Feb 10Good discussion here from Jamis on using returning to do the same thing.
Berserk
on 09 Feb 10I don’t mean that it should be line up perfectly.. and of course, if the HTML is all generated it is the job of the generator to to it.
I also prefer to generate XML nicely indented. This, if something, is not intended to be read by humans. As it turns out though, it often is. And when it is it is nice to be able to read it.
My preference to have whitespace where it should be, and not where it shouldn’t be, might stem from incidents where a single space f-up the layout (in NS 4.61, but anyway :)).
(Currently the comment form is linked to the href in the previous comment.)
Andy
on 09 Feb 10Isn’t this what partials are for?
Paul Thrasher
on 09 Feb 10@Berserk: I dunno if you’ve checked out the html source at google.com but I think you’ll find that, if anything, you’re better off compressing your html to get it to the browser fastest.
I used to do the same, lining up all my html/css/js just to get everything perfect, until I learned how wasteful it is to send a ton of uncompressed files to the browser.
DHH
on 09 Feb 10Andy, not at this level. You shouldn’t make a partial for a one-liner like that. It would be a terribly anemic partial and would only make the code harder to read.
Paul, gzip etc usually takes care of that.
Berserk, a DOM browser is better at lining everything up for you anyway. So relying on that seems to be the better practice.
Eric
on 11 Feb 10Nice. This makes the inner loop a bit easier to re-use, or refactor, as well. The same inner loop will run with any given “comment” (instead of being tied to message.comments.last)
Mathieu
on 12 Feb 10I totally agree with @dhh. The DOM browser is now built-in in most browsers anyway.
This discussion is closed.