18

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.

1
  • 1
    I think you are trying to put inside a controller a method that may be more logically placed inside a helper. Commented Jun 1, 2009 at 22:54

4 Answers 4

23
+100

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. :)

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the thorough feedback, Ian. As you can see from my updated situation, my specific case is that each controller's index method sets @offset and @limit, and I want a common method to come in behind that and set @next_url so my templates can use all three. So you would probably agree that it's awkward to move very typical index method code into a before filter just to fit the API. Thoughts?
Using a before_filter in ApplicationController for that is only awkward to the extent that without any further customization it will apply to every action in every controller. I prefer to be more scalpeled. Using a helper method (lowercase h, protected ApplicationController method) that gets called everywhere needed is actually not terrible either. Not everything has to be magic, calling methods like that has been done for years. Using a Helper method (uppercase H, app/helpers/application_helper.rb) to generate the URL is also a prime candidate since it's actually just view information.
Thanks again for your response, Ian. I did in fact go with the lower-case-h helper solution, with a plan to refactor based on what I learn here. I agree: not everything need be magic; however I thought it's a common pattern to repeat the same task after several actions but before rendering. I read elsewhere that "filter" is the opportune word, and it's bad form to use filters as generic pre-processors. That's reasonable. I am also considering wrapping either the index() method or perhaps render().
Final thought. I re-read your suggestion about just an orthodox helper, and that may be a good fit. I just need to confirm whether url_for() with no argument returns the RESTful URL for the current resource, which is what happens when I call it from the controller. If so, then next_url could just return url_for(:next => @next + @skip, :skip => @skip)
Glad you're getting some food for thought. As far as how common the pattern is, no other cases stand out in my memory in over 3 years of Rails development where identical code needed to be run after an action but before rendering. It's always been the case that all of the code that was shared could fit in a before filter. Even if it has to access the model it's managing (which will be different per controller), if you follow Rails naming conventions you can always do something like controller_name.singularize.camelize.constantize to access it. :)
4

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 :)

1 Comment

Thanks, Matthew. That is a great idea. I didn't think of it since @foo is an integer and I need to compute a RESTful URL, but hey I don't see why not! I will give this some consideration and update the question. I may bounty this answer and that puts you in top contention!
4

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

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.