I've been working with Rails for a while now and one thing I find myself constantly doing is checking to see if some attribute or object is nil in my view code before I display it. I'm starting to wonder if this is always the best idea.
My rationale so far has been that since my application(s) rely on user input unexpected things can occur. If I've learned one thing from programming in general it's that users inputting things the programmer didn't think of is one of the biggest sources of run-time errors. By checking for nil values I'm hoping to sidestep that and have my views gracefully handle the problem.
The thing is though I typically for various reasons have similar nil or invalid value checks in either my model or controller code. I wouldn't call it code duplication in the strictest sense, but it just doesn't seem very DRY. If I've already checked for nil objects in my controller is it okay if my view just assumes the object truly isn't nil? For attributes that can be nil that are displayed it makes sense to me to check every time, but for the objects themselves I'm not sure what is the best practice.
Here's a simplified, but typical example of what I'm talking about:
controller code
def show
#item = Item.find_by_id(params[:id])
#folders = Folder.find(:all, :order => 'display_order')
if #item == nil or #item.folder == nil
redirect_to(root_url) and return
end
end
view code
<% if #item != nil %>
display the item's attributes here
<% if #item.folder != nil %>
<%= link_to #item.folder.name, folder_path(#item.folder) %>
<% end %>
<% else %>
Oops! Looks like something went horribly wrong!
<% end %>
Is this a good idea or is it just silly?
No you should use
<% if #item.nil? %>
for example
#item1=nil
if #item1.nil? ### true
#item2 = ""
if #item2.nil? ### false
#item3 = []
if #item3.nil? ### false
#item4 = {}
if #item4.nil? ### false
To check An object is blank if it‘s false, empty, or a whitespace string.
use
<% if #item.blank? %>
ref:- this
for example
#item1=nil
if #item1.blank? #### true
#item2 = ""
if #item2.blank? #### true
#item3 = []
if #item3.blank? #### true
#item4 = {}
if #item4.blank? #### true
Your example code remade:
controller code. ( I assume this is ItemsController )
def show
# This will fail with 404 if item is not found
# You can config rails to pretty much render anything on Error 404
#item = Item.find(params[:id])
# doesn't seem to be used in the view
# #folders = Folder.find(:all, :order => 'display_order')
# this is not needed anymore, or should be in the Error 404 handler
#if #item == nil or #item.folder == nil
# redirect_to(root_url) and return
#end
end
view code, since the controller made sure we have #item
#display the item's attributes here
<%= item_folder_link(#item) %>
helper code:
# display link if the item has a folder
def item_folder_link(item)
# I assume folder.name should be a non-blank string
# You should properly validate this in folder model
link_to( item.folder.name, folder_path(item.folder) ) if item.folder
end
Anyway, I try to keep view very very simple. Usually if I see loops and conditionals in views, I try to refactor them into helpers.
Don't forget .try, which was added in Rails 2.3. This means that you can call something like the following:
#object.try(:name)
And if #object is nil, nothing will be returned. This is perhaps the built-in solution for sameera207's idea.
Ideally, you shouldn't be sending nil objects through to the view - however it's not always possible to avoid.
I personally think that if you are checking nil in your views (and I think since the view is the ultimate presentation layer nil should be checked in that level), you don't want to check it in the controller. (but this will not apply to all the places)
I would recommend you to create a method to check nil (to make it little DRY) and pass your object and check if it is nil or not
something like:
def is_nil(object)
object.nil? ? '':object
end
and add it in the application controller and make it a helper (so that you can use it in both controllers and views)
(helper_method :is_nil - add this line to your application controller)
and now you can pass the object you want to check if it is nil or not.
cheers,
sameera
Your controller is responsible for deciding which view is going to be rendered. If you can verify that your controller will never render this particular view without an item or item_folder then you do not need to check for nil values.
By can verify I mean that you have tests/specs that check which view is rendered for nil items and item_folders.
Related
I'm trying to use a helper method to determine the value of an attribute for several records. Here is the basic function I am trying to get working (from the view):
<% if Baseline.where(subject_id: sub.subject_id).first.crf_status(crf) == 1 %>
<td bgcolor="#98FB98" >
<% else %>
My helper function is crf_status(crf), and it looks like this:
application_helper.rb
def crf_status(crf)
case crf
when Baseline then 'baseline_status'
when FollowUp3Week then 'follow_up_3_week'
...
end
end
So a working example would be if crf_status(Baseline) would return:
<% if Baseline.where(subject_id: sub.subject_id).first.baseline_status == 1 %>
<td bgcolor="#98FB98" >
<% else %>
Right now, the error is 'undefined method 'crf_status' for Baseline'. So based on what I've read, perhaps I have to reference ApplicationHelper in each controller? That doesn't sound right. Please let me know what you think.
Thanks.
edit. I forgot to make this more clear: crf_status(crf) is being passed an object from an array [Baseline, FollowUp3Week...].
The actual line starts with it as well -> if crf.where(subject_id:...
When you do method chaining like .first.crf_status(crf) you don't get a fresh global scope every time. I.e. to get this example to work your crf_status would need to be defined as an instance method on the Baseline model.
From a MVC design perspective, it's frowned upon to do database queries (i.e. where) from your views; you should do it from the controller instead. The choice to use helpers here is totally optional. By putting it in a helper all you're doing is making it inaccessible from code outside your views.
To cut to the chase, here's what you should write in your Baseline model file:
def crf_status(crf)
case crf
when Baseline then baseline_status
when FollowUp3Week then follow_up_3_week
end
end
Note that the baseline_status and follow_up_3_week are actually method calls with the implicit receiver self.
You are calling "crf_status" on an instance of a model, helpers can only be called on views and controllers.
You have to do something like this
<% if crf.where(subject_id: sub.subject_id).first.send(crf_status(crf)) == 1 %>
<td bgcolor="#98FB98" >
<% else %>
Anyway, that looks like a weird code smell (making queries on view is not right and that crf_status looks like something that you should move inside your models)
If you want to return a method that is to be called in the context, use the .send method.
Baseline.where(subject_id: sub.subject_id).first.send(crf_status(crf))
Whatever is returned from your method will be executed. This is a great metaprogramming example. You want to test against the class of the instance, so use the .class method on your case line. You'll want to return symbols not strings though, so do this:
def crf_status(crf)
case crf
when Baseline then :baseline_status
when FollowUp3Week then :follow_up_3_week
else :default
end
end
Edit: Changed case for type comparison
I'm displaying certain items in my navigation bar depending on the current page. When I go to my sign in page the correct items are displayed. If I sign in with an incorrect password the items change and are incorrect.
In my html I check if (current_page?(new_user_session_path))
After the incorrect password is submitted and the page reloads this condition isn't returning true and it's displaying the wrong items in the navbar. I looked through the requests on the server logs and I'm guessing it's because the second time around the page loads after a POST (the unsuccessful password submission). Is there a different path I need to check for the second time?
Expanding on Scott's answer, you could create a helper in app/helpers/navigation_helper.rb for instance, like so:
module NavigationHelper
def current_location?(*args)
options = args.extract_options!
options.each do |key, val|
return false unless eval("controller.#{key.to_s}_name") == val
end
true
end
end
And use it this way:
current_location?(controller: 'my_controller', action: 'new')
current_location?(controller: 'my_controller')
current_location?(action: 'new')
In your view you can then do something like:
# Change this according what your really need
if current_location?(controller: 'sessions', action: 'new')
Hope it helps ; )
If you look at the source code of current_page?, it always returns false if the request's HTTP mode is anything other than GET or HEAD:
http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-current_page-3F
def current_page?(options)
unless request
raise "You cannot use helpers that need to determine the current " "page unless your view context provides a Request object " "in a #request method"
end
return false unless request.get? || request.head?
...
So even if your incorrect form is at exactly the same path as new_user_session_path, your logic won't match.
You may want to consider comparing controller.controller_name and controller.action_name directly instead. Not exactly elegant, but it's going to be more reliable.
I've just started my own rails project from scratch. I'm trying the view to display hello if the minutes variable is set to a certain value.
The code is in the controller right now, and I want it to display the output in the view. is this possible? or do I write it in the view? Not sure of doing it right.
home_controller.rb
class HomeController < ApplicationController
def index
#minutes = 23
#minutes = params[:minutes]
end
end
index.html.erb
<% if #minutes == 23 %>
<%= "helllo" %>
<% else %>
Anonymous
<% end %>
#minutes = params[:minutes] || 23
It's good enought in this simple case put some logic in the view but usually is better to add helper methods (Read the Getting Started guide is a good idea).
Probably you've already seen the app/helpers directory, in these helper files you can define methods which are available in the view layer, methods related to view layer but do stuff that would be weird or dirty to put in templates files.
For example in your case you could have a /app/helpers/time_helper.rb:
module TimeHelper
# I know the method name sucks a little
def show_hello_if_minutes_is_23(minutes = #minutes)
if minutes==23
"Hello"
else
"Anonymous"
end
end
end
and then use in your index.html.erb template:
<%= show_hello_if_minutes_is_23 %>
As you can see:
You can read the method name and understand what it does (at high level)
Logic is put in a ruby method
The method take a minutes argument but it's optional
And remember: usually repetition is evil (The DRY thing) but in view-land sometimes one time is too much (not in this simple case however).
UPDATE: I've just seen you put set the #minutes variable to 23 and then you overwrite it making the previous assignment useless. I don't know what you're trying to do in your controller but if your question is about having a default value for the minutes variable go with the Yuri's answer and use use the ||= operator: #minutes ||= params[:minutes].
I have been searching through Stack Overflow for a few hours now, but none of the related questions seem to apply to my issue.
I am new to Rails, with this being my first real project, and I may be confusing the MVC setup a little. I am attempting to assign the #stars instance variable while in an action of the searches_controller.rb:
def create
#search = Search.new(params[:search])
tempstr = searchstr(#search)
#stars = Star.where("tempstr", :limit => 100)
end
#search is created fine, being a complex search with varying parameters. tempstr is just a simple string container for the results of searchstr(#search), which is a quick method for converting the search parameters into a MySql-relevant string (which seems to be easier than trying to use the .where helper normally, in this case). I'm sure I can just put searchstr(#search) directly into the .where, but I split them up for now so I can inspect the elements as they pass through.
Anyways, the issue comes up when I try to call #stars in the show.html.erb view. Even with something as simple as this:
<% #stars.each do |star| %>
<%= display stuff %>
<% end %>
I get an error saying 'each' is not a method of nil:NilClass. So, I changed it to the following to see if #stars was nil:
<%= #stars.inspect %>
Sure enough, #stars is nil. However, when I add this line to my controller to check #stars there:
return render #stars.each
I see that the variable is filled with the correct star objects from the Star.where(), just as I had intended. A quick .inspect shows the variable is not nil, when in the controller.
So, I am unsure why the view is receiving it as nil if it has been defined in the controller just fine. I wouldn't be surprised if it was me misunderstanding how MVC works, though. The Star class was defined in the Star model, but maybe it is because I am trying to access it from the Searches controller, and thus it isn't initialized for the view?
Should I be going about doing this some other way? I attempted to use a local variable (using stars instead of #stars), but then the view says "Undefined local variable or method 'stars'".
Any help would be much appreciated, I have already wracked my brain for hours creating the complex search and parsing the star file data into the database, so I'm a bit burnt out. I can supply more information if requested, I'm not sure what else would be helpful in providing an answer.
You are setting #stars in the create method, but the view you are talking about is show.html.erb. Try setting #stars in the show method too. Something like this:
def show
#search = Search.find(params[:id])
tempstr = searchstr(#search)
#stars = Star.where("tempstr", :limit => 100)
end
If this does not help you, please show the rest of you controller actions, so we can help you better.
I'm trying to display a javascript feedback widget in my default application.rhtml in a rails app. It will only appear on a subset of pages, distributed across different controllers.
Trying to figure out the best way to do this.
One thought was to do something like this:
<%= render :partial => "layouts/feedback_tab" if #show_feedback_tab == true %>
and then setting #show_feedback_tab in every method in every controller. this seems overly complex. my second thought was that i could default #show_feedback_tab to true and set it to false for the relevant individual methods where i don't want to show it. but a global var doesn't seem right, and a method in application_controller won't work (i think) as the display is dependent on the method that's being called.
Any thoughts?
You can write method in application_controller.rb:
def show_feedback_tab?
if params[:controller] == :user && params[:action] == :index
return true
end
...
or put here any other logic
...
false
end
and add it as a helper method (in application_controller.rb):
helper_method :show_feedback_tab?
then you can use it in views like this:
<%= render :partial => "layouts/feedback_tab" if show_feedback_tab? %>