I have a rails template (.rhtml file) generating a Javascript object. It looks something like the following:
var volumes = {
<% for volume in #volumes %>
<%= volume.id %> : <%= volume.data %>
<%= ',' unless volume === #volumes.last %>
<% end %>
};
Note the unless statement modifier to suppress printing the comma after the last element (to satisfy Internet Explorer, which incredibly doesn't support trailing commas in JSON properties declarations).
This appears to work, but as a matter of style, do people think it is reasonable to rely on <%= value unless condition %> in the template generating an appropriate render call?
I don't see why not, but generally if you find yourself conditionalizing a comma on the last member, you probably want to use join instead:
<%= #volumes.map {|v| "#{v.id} : #{v.data}"}.join "," %>
If you would like to contruct JSON (and BTW you are constructing JavaScript Object not Array) then I suggest to use to_json method:
var volumes = <%= #volumes.inject({}){|h,v| h.merge(v.id=>v.data)}.to_json %>;
or
var volumes = <%= Hash[*#volumes.map{|v| [v.id, v.data]}.flatten].to_json %>;
Even better would be to move Ruby Hash construction to model as it is too complex for view.
class Volume
def self.to_hash(volumes)
Hash[*volumes.map{|v| [v.id, v.data]}.flatten]
end
end
and then in view you can put much simpler code:
var volumes = <%= Volume.to_hash(#volumes).to_json %>;
Or even:
<%= #volumes.map { |v| "#{v.id} : #{v.data}"}.to_sentence -%>
To get "a: something, b: something else, c: anything, and d: another thing."
Related
I have an instance variable #tally_property, and if there are photos on that object I would like to cycle through the photos and show them.
So my code snippet looks like this:
<% if #tally_property.photos.present? %>
<% #tally_property.photos.each_with_index do |photo, index| %>
The issue is that based on the above, if #tally_property is nil, then the entire first line throws an error.
So is there a 'nil' check I can do that isn't bulky, i.e. I don't want to do if #tally_property.nil?, on both the primary object and the association, and is elegant and ruby & rails-esque?
I would use the safe navigation operator (&.) and write something like this:
<% #tally_property&.photos&.each_with_index do |photo, index| %>
...
<% end %>
In Ruby 2.3.0+ you can use the safe navigation operator:
#tally_property&.photos
ActiveSupport has a .try method that can be used to the same end in older versions of ruby:
#tally_property.try(:photos)
You can add a simple conditional to be able to safely iterate through the collection:
<% (#tally_property.try(:photos)||[]).each_with_index do |photo, index| %>
<% end %>
Rails 4 adds ActiveRecord::Relation#none and a change in behaviour so that associations always return a ActiveRecord::Relation. So its perfectly acceptable to write:
<% #tally_property.try(:photos).try(:each_with_index) do |photo, index| %>
<% end %>
After upgrading your app. Or you can use a partial and render:
<%= render partial: 'photos', collection: #tally_property.photos if #tally_property %>
Which removes the need for writing the iteration.
Use && (or and, they each have their sweetspot).
Taking it out of Erb for a moment, I would generally write something like this:
if #tally_property and #tally_property.photos.present?
Depending on photos I might use:
if #tally_property and #tally_property.photos
or perhaps:
if #tally_property and not #tally_property.photos.empty?
Sometimes I'll use a temporary variable:
if (photos = #tally_property && #tally_property.photos)
photos.each #…
That kind of thing.
I would recommend this episode of Ruby Tapas, And/Or for a longer (but still quick) look at it.
One more way, just select all photos connected to this tally_property:
example how it might be:
Photo.joins(:tally_property).each_with_index do |photo, index|
Using Rails 3. I have 2 options for my view:
OPTION A:
<% #items.each do |item| %>
...
<% end %>
...
<script>
var items = [
<% #items.each do |item| %>
['<%= item.name %>', <%= item.lat %>, <%= item.lng %>],
<% end %>
];
</script>
OPTION B:
<% #items.each do |item| %>
...
<% content_for :items_array do %>
['<%= item.name %>', <%= item.lat %>, <%= item.lng %>],
<% end %>
<% end %>
...
<script>
var items = [<%= yield :items_array %>];
</script>
At the moment, I have chosen OPTION B, but I still think that it is not neat at all. Any better way to do this? I refused to choose OPTION A because I don't want it to loop twice in the same view because of performance and DRY issues, correct me if I am wrong.
Thanks.
If it were my project I would probably move this problem to the javascript side. Put the lat and long into data- attributes somewhere in the item tags and use unobtrusive scripting to read them. (If you want an example let me know.)
If that's not possible, I personally would prefer option A, first of all for readability (it's easier to understand, especially if you're not familiar with content_for) and second because it's easier to refactor, we could move collecting the data into a method on the model or maybe into a helper. DRY doesn't really apply here because even though you're looping through the same dataset, you're doing very different things with it.
That said, I wasn't quite sure about the performance implications so I ran a little test. I took 500 client records in one of my apps and used the option A to render a list of links followed by mapping their data into a content_for block and benchmarked it using Ruby's inbuilt Benchmark module (it's in haml instead of erb but I'm sure it's not too hard to read.)
- bench = Benchmark.measure do
%ul#client-list
- #clients.each do |client|
%li
= link_to client.company, client
- content_for :foobar do
= "['#{j client.name}', '#{j client.company}', '#{j client.email}'],"
= javascript_tag do
var nearbys = [#{yield :foobar}];
%p= bench.to_s
The result, about 370 miliseconds
0.370000 0.000000 0.370000 ( 0.397476)
Then I did the same with two loops (one each one map):
- bench = Benchmark.measure do
%ul#client-list
- #clients.each do |client|
%li
= link_to client.company, client
= javascript_tag do
= "var nearbys = [#{ #clients.map { |client| "['#{j client.name}', '#{j client.company}', '#{j client.email}']," }}]"
%p= bench.to_s
The results, about 80 miliseconds:
0.080000 0.000000 0.080000 ( 0.077940)
You may want to check the results yourself, but it seems like the content_for method is not a very efficient means to solve this problem.
edit: just for reference, this was tested using ree 1.8.7 and rails 3.2.6
edit2:
An example of how to rewrite this without using inline javascript. I'd first move all the required parameters into data- attributes, for example in the view:
<% #items.each do |item| %>
<div class="item" data-name="<%= item.name %>"
data-lat="<%= item.lat %>"
data-long="<%= item.long %>">
<%= item.name %>
</div>
<% end %>
(You could also take the name from inside the div, but putting everything in data-attributes is more consistent.)
Then in you javascript you could map the data as follows (assuming jquery):
var nearbys = $('.item').map(function() {
return [ $(this).data("name"), $(this).data("lat"), $(this).data("long") ];
});
This way you can keep all your javascript neatly in a separate file and collecting the data is done client-side.
it's very simple just use to_json it will do all the escaping for you ;)
So your code could look like this:
<script>
var items = <%= #items.map{|o| [o.name, o.lat, o.lng]}.to_json.html_safe %>;
</script>
(the html_safe marks the string as safe, avoiding double escaping)
If you have objects that are more complex than just arrays of strings and numbers you should use jQuery's $.parseJSON(json) so it would look like:
<script>
var items = $.parseJSON('<%= #items.map{|o| [o.name, o.lat, o.lng]}.to_json.html_safe %>');
</script>
Enjoy!
All of this code should go into a function in the helper file for the Model. That is what the helpers are for, right?
The views should have minimum Ruby code hard-wired into them. So, use helpers.
Here are some links to get you more insight into helpers:
using-helper-methods-in-ruby-on-rails
what-belongs-in-a-helper-method
I've got a Rails 3 application where I'm using quite a few conditional statements to change the design of the page. What is the best practice for keeping the logic out of the view for having such drastic amounts of conditionals?
Hypothetical Example:
<% unless #ethos.blank? %>
<%= unless #work.nil? do %>
<%= link_to "Add Work", work_path %>
<% end %>
<%= #ethos.tagline %>
<% end %>
I've got many more conditionals inside of other conditionals. What is the best way to manage this inside of one view?
You should avoid complex conditionals (and most conditionals) in views. Extract them to a Helper, or better yet, to some kind of "presenter" so that you can work with a receiver instead of those "global looking/feeling helpers"
SomeHelper
module SomeHelper
def work_link
(#ethos.present? && #work) ? link_to("Add Work", work_path) : nil
end
end
View
<%= work_link %>
<%= #ethos.tagline if #ethos.present? %>
If #ethos is likely to be nil as opposed to an empty [] array, you could instead use:
<%= #ethos.try :tagline %>
Also note that in your original view <%= unless #work.nil? do %> should have been using a - and not a =.
Oh, and I encourage you to use HAML over ERB. With HAML, the view looks like this (easier to read, isn't it) :
= work_link %>
= #ethos. try :tagline
Your original view would look like this in HAML (remember, avoid conditionals in views as much as possible!)
- unless #ethos.blank?
- unless #work.nil? do
= link_to "Add Work", work_path
= #ethos.tagline
If the code works, what is your concern? Is it aesthetics or are you having difficulty reasoning about what the code is doing because there is so much nesting?.
The simplest solution is probably just to move the conditionals inline.
<%= link_to("Add Work", work_path) if #ethos.present? && #work %>
<%= #ethos.tagline if #ethos.present? %>
This will improve readability (and therefore maintainability), though it may not go far enough to keep the Rails purists happy. Zabba's answer presents several great choices that go further down the rabbit hole.
How to add "0," after each loop in Ruby (At the data: part of the series)
So first loop would
show only the value
next loop 0,value
next loop 0,0,value
next loop 0,0,0,value
etc..
series: [
<% #prot = ProjectTask.where("project_id = ? AND taskType = ?" ,#project.id, "Pre-Sales")%>
<% #prot.each do |prt| %>
<% hoursSum = 0 %>
{
name: '<%= prt.task_name%>',
data: [(here after each loop of #prot i want to add "0," here)<% #taskHours = Effort.where(:project_task_id => prt.id) %>
<% #taskHours.each do |th| %>
<% hoursSum = hoursSum + th.hours %>
<% end %>
<%= hoursSum%>
]
},<% end %>
<% #prot.each_with_index do |index, prt| %>
...
<%= "0," * index %>
...
<% end %>
This logic seems too complex to be in a view - I would migrate it to a helper function that builds up the data array and call that instead.
I think you'll find it a lot easier to do what you want then without having to deal with all the clutter of the erb tags etc. There's lots of ways you could do it - Yossi's suggestion of using each_with_index is a perfectly good one.
Two little things though - I would advise against shortened names for stuff like #prot - just call it #project_task. It's more readable and you can guarantee you call it the same thing throughout your code (instead of having some places where you call it #prot, others where it's #ptask etc) which will save you more time than you lose typing a longer name, I promise you.
Also - you use camelCase for some of your variables - I would advise sticking with the Ruby convention of snake_case.
I have been trying to avoid using Response.Write(...) directly in my MVC Views. The reason being that I just need to type the string literals and the view engine knows what to do. However, in certain circumstances I seem to be creating way too many pointy brackets (<% %>). So, which of these 2 code snippets do you find more acceptable?
<% if (true)
{
Response.Write(Model.SomeValue);
} %>
Or
<% if (true) { %>
<%= Model.SomeValue %>
<% } %>
This is why Html Helpers exist (to avoid spaghetti code as much as possible):
<%= Html.MySuperHelper(Model.SomeValue) %>
Every time you need write an if statement in a view you might ask yourself the question: wouldn't it be better to write a helper method (which as a bonus could be unit tested) instead?
How about a third possibility?
<%= condition ? Html.Encode(Model.SomeValue) : "" %>
Although in practice you should keep all but the very simplest logic out of your view altogether. Either do the work in your controller or wrap the logic up in a HTML helper of some kind.
Or a fourth:
<%= condition ? Html.Encode(Model.SomeValue) : "" %>