views:

622

answers:

2

I'm running into some type of a scope issue that's preventing instance variables from being properly initialized by helpers called from the view.

#sample_controller.rb
class SampleController < ApplicationController
  def test
  end
end

#application_controller.rb
helper_method :display 
def display
  if not defined? @display
    return @display = "a"
  else
    return @display += "a"
  end
end

#test.html.erb
<%= display %>
<%= display %>
<%= @display %>
<%= @display.reverse %>

When sample/test is displayed, it dies with an error "while evaluating nil.reverse". This is surprising because the first two calls to display should have initialized @display I would have thought. If <%= @display.reverse %> is removed the output is "a aa", indicating that the @display instance variable is getting set by the helper method, but there is no access to it in the view.

If the controller is modified so to become (with the original view code):

class SampleController < ApplicationController
  def test
    display
  end
end

The output becomes "aa aaa a a". If I make it 2 calls to display in the controller, I get, "aaa aaaa aa aa". So it seems like only the calls made in the controller will modify the SampleController instance variable, whereas calls in the view will modify an instance variable for the ApplicationController that the view has no access to.

Is this a bug in Rails or am I misunderstanding and this is for some reason intended functionality?

The context in which I ran into this bug is trying to create a logged_in? ApplicationController method that sets up an @user variable the first time it is called, and returns true or false if a user is logged in. This would not work unless I added an unnecessary call in the controller before attempting to use it in the view.

A: 

I think this might be the second reserved words issue I've seen this week...

Tip: you almost never need to use the return keyword in Ruby. Also, your use of defined? is freaking me out...might be worth trying this:

  if @display.nil?
    @display = "a"
  else
    @display += "a"
  end
floyd
What's the reserved words issue you see here? At any rate, I'm guessing that's not the fundamental issue, since I originally encountered the bug using all different words (login stuff like I mentioned) and the code here I generated to figure out what the heck was going wrong.Thanks for the tip on the return statement.The defined? that I used seems to be doing what I intended though doesn't it? I got it from the sample implementation code for authlogic where they used it for a similar purpose.
WIlliam Jones
according to the reserved word list, "display" has been known to cause problems. I have never heard of a problem defining instance variables in views, since most login patterns do exactly what you're trying to do here, defining @current_user when current_user is called for the first time. defined? will technically work here, but it is usually used for things that raise an error rather than return nil if undefined, like local variables and method definitions. nil? works fine for the very common case of instance variable checks and is slightly easier to parse.
floyd
+1  A: 

There are two @display ivars at render time, one in the controller and one in the view. The view can't access the one in the controller and vice versa. At the start of the render Rails copies all the controller ivars into the view. But at that time @display does not exist and is not copied over. In any ruby program a call to @my_undefined_ivar will return nil - which is exactly what is happening to you.

Could be confusing, here it is said another way. Rails copies ivars from the controller into the view at the start of the render. So, changes in the view's ivar are not reflected in the controller ivar and vice versa. The helper you defined lets the view invoke a method on the controller so the controller's ivar can be passed back to the view as a return value but the change to the controller ivar itself is not reflected in the other, the view's, ivar.

Just use the helper method in this case and forget about @display in the view.

glongman
So in my real app, @display is actually @user, and it is used for displaying information about the user. As an example of what I mean, typical code is something like, <%if logged_in?%><%[email protected]%><%end%>. So is the best way to do this then to have a before_filter call in the ApplicationController that sets up the @user variable? Because often, for a given view the only use of @user is for something like I outlined above, and there's no other need for it to be used in the controller itself.
WIlliam Jones
Yes, the ivar copy mechanism fits what you describe. Setup @user in a before filter.
glongman