249

I've been a bad kid and used the following syntax in my partial templates to set default values for local variables if a value wasn't explicitly defined in the :locals hash when rendering the partial --

<% foo = default_value unless (defined? foo) %>

This seemed to work fine until recently, when (for no reason I could discern) non-passed variables started behaving as if they had been defined to nil (rather than undefined).

As has been pointed by various helpful people on SO, http://api.rubyonrails.org/classes/ActionView/Base.html says not to use

defined? foo

and instead to use

local_assigns.has_key? :foo

I'm trying to amend my ways, but that means changing a lot of templates.

Can/should I just charge ahead and make this change in all the templates? Is there any trickiness I need to watch for? How diligently do I need to test each one?

brahn
  • 12,096
  • 11
  • 39
  • 49
  • 1
    This question is quite old, and all answers seemed to be outdated, because the current [Action View documentation](https://api.rubyonrails.org/classes/ActionView/Base.html), specifically says it's okay to use `definded? foo`: _"Alternatively, you could also use defined? headline to first check if the variable has been assigned before using it."_ – wnm Feb 23 '21 at 10:10

12 Answers12

359

I do this:

<% some_local = default_value if local_assigns[:some_local].nil? %>
Makoto
  • 104,088
  • 27
  • 192
  • 230
jonnii
  • 28,019
  • 8
  • 80
  • 108
  • 1
    Though I really like the compact syntax of hgimenez's suggestion (above), this approach has the advantage of being very clear re: what's going on. (Still has the downside of not letting you pass nil as a value for the local) – brahn Jan 18 '10 at 03:59
  • 1
    What's your use case for wanting to pass nil? – jonnii Jan 29 '10 at 15:57
  • Oh, I don't have a specific case in mind. Just trying to understand the full implications. This reminded me to accept this answer :-) – brahn Jan 29 '10 at 23:59
  • 4
    To get over the nil issue, I'm copying the code from the OP's link (http://api.rubyonrails.org/classes/ActionView/Base.html) <% if local_assigns.has_key? :headline %> Headline: <%= headline %> <% end %> -- has_key avoids the nil / false situation, and probably can be shortened to one line like the answer here – Phil Apr 10 '13 at 13:31
  • 7
    Please check Pablo's answer: `local_assigns.fetch` perfectly handles even keys with nil value. It returns a default only if the key is **not** set at all. – quetzalcoatl Aug 14 '13 at 19:10
  • I know this is old. But I want to add a new solution `<% some_local ||= default_value %>` – donkey Apr 25 '16 at 00:06
179

Since local_assigns is a hash, you could also use fetch with the optional default_value.

local_assigns.fetch :foo, default_value

This will return default_value if foo wasn't set.

WARNING:

Be careful with local_assigns.fetch :foo, default_value when default_value is a method, as it will be called anyway in order to pass its result to fetch.

If your default_value is a method, you can wrap it in a block: local_assigns.fetch(:foo) { default_value } to prevent its call when it's not needed.

Pablo Cantero
  • 6,239
  • 4
  • 33
  • 44
  • 1
    It's worth saying it explicitly: `nil` values are preserved here. If the hash contains `:foo` mapped to `nil`, then `fetch` it will return `nil`. That is, at least on my v1.9.3. I don't remember how 1.8 behaved. – quetzalcoatl Aug 14 '13 at 19:14
  • That's totally right. It remembers me the problem for `local_assigns[:foo] || default_value`, when foo returns a falsy value, the `default_value` will be used instead. It is usually a problem for Memoization `@some_value ||= expensive_method` if the method returns a falsy value, it will always be executed. – Pablo Cantero Aug 15 '13 at 15:25
  • 1
    Anyone who doesn't understand why this is the best answer hasn't used ruby long enough. Bravo Pablo! – mastaBlasta Mar 26 '14 at 14:56
  • 2
    An optimization is the following, so that you only have to call this once at the top of your template, instead of using the 'fetch guard' on every use of the variable. `foo ||= local_assigns[:foo] = local_assigns.fetch(:foo, default_value)` – sethcall Mar 30 '14 at 18:41
  • 1
    I was wondering why it didn't work, I assumed it created the variable too, but we still have to use the returned value: `foo = local_assigns.fetch :foo, true` – Vadorequest Apr 01 '14 at 11:30
  • If you don't want to repeat yourself every time (maybe you are checking foo multiple times, you can do `<% foo = local_assigns.fetch(:foo, foo_default_value) %>` at the top of your partial. – sandre89 Dec 12 '18 at 21:46
89

How about

<% foo ||= default_value %>

This says "use foo if it is not nil or true. Otherwise assign default_value to foo"

hgmnz
  • 13,208
  • 4
  • 37
  • 41
  • 2
    I'm not sure this works as foo isn't defined unless it's passed in through the locals hash. – jonnii Jan 13 '10 at 23:13
  • Yeah, it works. There is some magic that allows you to do this with undefined locals. – mckeed Jan 13 '10 at 23:38
  • 1
    This works, but if you have default values like this, maybe it's a sign that you should use a helper? – psyho Jan 13 '10 at 23:42
  • 2
    No magic here. More resources on the subject: http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/fe4fbc48e19105cd/bf7f73380e285aff?lnk=gst&q=Or+equal – hgmnz Jan 13 '10 at 23:49
  • 38
    I really like this version since the syntax is so compact. I suppose the big downside is that it means you can't pass nil or false as a value for the local, though, since it will be overwritten by the default. – brahn Jan 18 '10 at 03:52
  • 17
    @brahn, that's a good point. In fact, this should be avoided if `foo` is a boolean. It may rightfully have the value of `false`, and be overridden by `default_value` accidentally. – hgmnz Jan 18 '10 at 13:21
  • 1
    Please (also) check Pablo's answer: `local_assigns.fetch` perfectly handles even keys with nil value. It returns a default only if the key is **not** set at all. – quetzalcoatl Aug 14 '13 at 19:11
  • 1
    @hgmnz isn't this behavior only a problem if the default value is `true`? – Ziggy Aug 03 '15 at 21:09
  • Note that `<%= foo || 'default' %>` doesn't work, but `<%= foo ||= 'default' %>` works fine. – collimarco Dec 03 '20 at 16:59
13

I think this should be repeated here (from http://api.rubyonrails.org/classes/ActionView/Base.html):

If you need to find out whether a certain local variable has been assigned a value in a particular render call, you need to use the following pattern:

<% if local_assigns.has_key? :headline %>
  Headline: <%= headline %>
<% end %>

Testing using defined? headline will not work. This is an implementation restriction.

gamov
  • 3,789
  • 1
  • 31
  • 28
11

In my case, I use:

<% variable ||= "" %>

in my partial.
I don't have idea if that is good but for my is OK

Moises Portillo
  • 828
  • 8
  • 12
  • This actually works quite well. Works for undefined and when passing `nil` as a local in the partial call as well. – Joshua Pinter May 20 '17 at 15:39
  • 6
    Ah, reading down below, this will fail if `variable` is a boolean and you need to set it to `false`. It will use the default value instead of using the `false` value. USE AT YOUR OWN RISK. – Joshua Pinter May 20 '17 at 15:40
  • This is the cleanest way for anything without truthy confusion. – Peter DeWeese Jul 21 '21 at 16:21
5

I know it's an old thread but here's my small contribution: i would use local_assigns[:foo].presence in a conditional inside the partial. Then i set foo only when needed in the render call:

<%= render 'path/to/my_partial', always_present_local_var: "bar", foo: "baz" %>

Have a look at te official Rails guide here. Valid from RoR 3.1.0.

microspino
  • 7,693
  • 3
  • 48
  • 49
  • I can't see any real difference between `local_assigns[:foo]` and `local_assigns[:foo].presence`. Either one will return `nil` if the key does not exist in the hash and the value if it does exist. – James Jul 10 '17 at 09:12
3

This is a derivative of Pablo's answer. This allows me to set a default ('full'), and in the end, 'mode' is set in both local_assigns and an actual local variable.

haml/slim:

- mode ||= local_assigns[:mode] = local_assigns.fetch(:mode, 'full')

erb:

<% mode ||= local_assigns[:mode] = local_assigns.fetch(:mode, 'full') %>
sethcall
  • 2,837
  • 1
  • 19
  • 22
1

Ruby 2.5

Erb

It's possible, but you must to declare your default values in the scope.

VARIABLE the word for replacement.

# index.html.erb
...
<%= render 'some_content', VARIABLE: false %>
...

# _some_content.html.erb
...
<% VARIABLE = true if local_assigns[:VARIABLE].nil? %>
<% if VARIABLE %>
    <h1>Do you see me?</h1>
<% end %>
...
dimpiax
  • 12,093
  • 5
  • 62
  • 45
1

I think a better option that allows for multiple default variables:

<% options = local_assigns.reverse_merge(:include_css => true, :include_js => true) %>
<%= include_stylesheets :national_header_css if options[:include_css] %>
<%= include_javascripts :national_header_js if options[:include_js] %>
0

If you do not want to pass local variable to partial each time you call it you do this:

<% local_param = defined?(local_param) ? local_param : nil %>

This way you avoid undefined variable error. This will allow you to call your partial with/without local variables.

TomFuertes
  • 7,150
  • 5
  • 35
  • 49
Haris Krajina
  • 14,824
  • 12
  • 64
  • 81
0

More intuitive and compact:

<% some_local = default_value unless local_assigns[:some_local] %>

muirbot
  • 2,061
  • 1
  • 20
  • 29
  • 4
    I think this will fail if you call the partial with `:locals => {:some_local => false}` – brahn Nov 17 '11 at 17:23
-6

A helper can be created to look like this:

somearg = opt(:somearg) { :defaultvalue }

Implemented like:

module OptHelper
  def opt(name, &block)
    was_assigned, value = eval(
      "[ local_assigns.has_key?(:#{name}), local_assigns[:#{name}] ]", 
      block.binding)
    if was_assigned
      value
    else
      yield
    end
  end
end

See my blog for details on how and why.

Note that this solution does allow you to pass nil or false as the value without it being overridden.

Jaime Cham
  • 1,494
  • 1
  • 15
  • 16