Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails: Set a common or global instance variable across several controller actions

How should one have several different controller' actions set a common instance variable for use in templates but after the action runs.

In other words, I want this to work in my application_controller.

class ApplicationController < ActionController::Base
  after_filter :set_something_common

  def set_something_common
    # All controllers' actions have queried the DB and set @foo for me...
    @bar = some_calculation_on(@foo)
    # ... and all templates expect @bar to bet set.
  end
end

This does not work because after_filter runs after rendering. Fine. But what is the correct pattern?

Again, it is important that set_something_common runs after the action because those actions do case-specific things; but they all set @foo.

None of my ideas seem ideal:

  • Call set_something_common() towards the bottom of every action that needs it.
  • Refactor all controllers' case-specific code into case_specific_code() and force them to run in order:

    before_filter :case_specific_code, :set_something_common
    
  • Subclass application_controller and redefine the index method.

Any thoughts? Thanks.

Edit: Matthew's response prompted me to clarify:

Several controlers' index() all do pagination, each taking parameters @offset and @limit (via a global before_filter) to view data slices. Great. Now I want a common method to compute a RESTful URL for the "next slice" link. I was encouraged to see that url_for() generates a URL returning to the same resource, so I tried:

def set_something_common # really called set_next_url, truth be told
  @next_url = url_for(:offset => @offset + @limit, :limit => @limit)
end

I will try monkey patching Fixnum, so I can do something like @offset.next_url_for(self, @limit) from the template, but I'm not sure if it will work. Come to think of it, if I am going to modify the templates, then I may as well set up an application helper. I'm still not sure what the best solution is.

Update: Accepted answer is "use a helper."

Thanks for the updates from everybody. I learned my lesson that helpers, like global variables, are there for a reason and not to be eschewed when they are plainly beneficial and succinct.

like image 505
JasonSmith Avatar asked May 27 '09 06:05

JasonSmith


4 Answers

Firstly, you don't want to try to insert code "between" a controller action and a template rendering. Why? Because you want the controller action to have the freedom to choose what sort of response to give. It could return XML, JSON, headers only, a redirection, nothing, etc. That's why after filters are executed after the response has been rendered.

Secondly, you don't want to monkey patch Fixnum. I mean, maybe you do, but I don't. Not often at least, and not unless I get some totally wicked semantic benefits from it, like being able to say 3.blind_mice. Monkey patching it for a random use case like this seems like a maintenance headache down the road.

You mention refactoring out all the controllers' case specific code into a before filter and running them sequentially. Which brings up to my mind... @foo is the same in every case? If that's the case, then one before filter would work just fine:

before_filter :do_common_stuff
def do_common_stuff
  @foo = common_foo
  @bar = do_something_with @foo
end

That's a totally legit approach. But if @foo changes from controller to controller... well, you have a few more options.

You can separate your before filters into two halves, and customize one per controller.

# application_controller:
before_filter :get_foo, :do_something_common
def do_something_common
  @bar = do_something_with @foo
end

# baz_controller:
def get_foo
  @foo = pull_from_mouth
end

#baf_controller:
def get_foo
  @foo = pull_from_ear
end

But you know, if it's a simple case that doesn't need database access or network access or anything like that... which your case doesn't... don't kill yourself. And don't sweat it. Throw it in a helper. That's what they're there for, to help. You're basically just rearranging some view data into a form slightly easier to use anyway. A helper is my vote. And you can just name it next_url. :)

like image 144
Ian Terrell Avatar answered Oct 12 '22 05:10

Ian Terrell


I would have a method on @foo which returns a bar, that way you can use @foo.bar in your views.

<% @bar = @foo.bar %> #if you really don't want to change your views, but you didn't hear this from me :)

like image 42
MatthewFord Avatar answered Oct 12 '22 05:10

MatthewFord


Use <%= do_some_calculations(@foo) %> inside your templates. That is the straight way.

like image 31
Bogdan Gusiev Avatar answered Oct 12 '22 05:10

Bogdan Gusiev


I had this challenge when working on a Rails 6 application.

I wanted to use an instance variable in a partial (app/views/shared/_header.html.erb) that was defined in a different controller (app/controllers/categories_controller.rb).

The instance variable that I wanted to use is @categories which is defined as:

# app/controllers/categories_controller.rb

class CategoriesController < ApplicationController

  def index
    @categories = Category.all
  end
  .
  .
  .
end

Here's how I did it:

Firstly, I defined a helper_method for the instance variable in my app/controllers/application_controller.rb file:

class ApplicationController < ActionController::Base
  helper_method :categories

  def categories
    @categories = Category.all
  end
end

This made the @categories instance variable globally available as categories to every controller action and views:

Next,I rendered the app/views/shared/_header.html.erb partial in the app/views/layouts/application.html.erb this way:

<%= render partial: '/shared/header' %>

This also makes the @categories instance variable globally available as categories become available to every controller views that will use the partial without the need to define the @categories instance variable in the respective controllers of the views.

So I used the @categories instance variable globally available as categories in the partial this way:

# app/views/shared/_header.html.erb

<% categories.each do |category| %>
  <%= link_to category do %>
    <%= category.name %>
  <% end %>
<% end %>

Note: You can use locals to pass in the variables into the partials:

<%= render partial: '/shared/header', locals: { categories: @categories } %>

However, this will require a controller action that sets a @categories instance variable for every controller views that will use the partial.

You can read up more about Helpers and Helper Methods in the Rails Official Documentation: https://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html

That's all.

I hope this helps

like image 31
Promise Preston Avatar answered Oct 12 '22 04:10

Promise Preston