简体   繁体   中英

Rails: dynamically include helpers per-request at runtime

My app is has regular users and admins. It has a few rendering helpers which display widgets:

module ApplicationHelper
  def widget
    "widget"
  end
end

For admins, there are extended versions of these widgets:

module AdminHelper
  def widget
    "admin-widget"
  end
end

The app is configured not to include all helpers by default through config.action_controller.include_all_helpers = false .

When an admin views a page - same controller, same view template -, I want to render an extended version of it by overloading helpers with their AdminHelper version. How do I overload ApplicationHelper with AdminHelper on a per-request basis if the currently logged in user is an admin?

I have tried

class ApplicationController < ActionController::Base  
  before_action :include_backend_helpers

    protected

  def include_backend_helpers
    self.class.helper "pica/backend" if admin_signed_in?
  end
end

but this doesn't work unless ApplicationController is reloaded between requests.

Trying to dynamically load code to override the widget is the wrong approach to take. Instead, the widget method should decide which version to display on a request-by-request basis. Before we look at some ways to do that, let's explain why the module overriding doesn't work.


ApplicationController is loaded when your Rails application starts. This automatically includes the ApplicationHelper module, which defines the widget method. The AdminHelper isn't loaded because of the config.action_controller.include_all_helpers setting - by setting it to false Rails will only load a helper with the same name as the controller.

When a regular user calls the widget method, the string "widget" is returned. But as soon as an admin visits the site the include_backend_helpers callback loads your AdminHelper module, overriding the widget method. The admin gets the string "admin-widget" returned - but so does every future visitor to the site! The widget method isn't differentiating based on user types.

You could solve this by reloading the ApplicationController after each request, but that takes time and will make performance suck. You might be able to unload the module , but that seems like an approach fraught with difficulty. You could also try reloading the helper on each request:

def include_backend_helpers
  if admin_signed_in?
    self.class.helper "admin/backend"
  else
    self.class.helper "user/backend"
  end
end

But I'm not sure that would work, and I wouldn't recommend it.


Instead, it seems that it's the widget's job to decide what's appropriate to display. For instance, if this was a navigation component, it should be smart enough to only show admin links. You can still break it out into separate methods:

module ApplicationHelper
  def widget
    if admin_signed_in?
      user_widget
    else
      admin_widget
    end
  end

  private
  def user_widget
    "widget"
  end

  def admin_widget
    "admin-widget"
  end
end

You can use a similar approach with Rails partials - you could have a _navigation.html.erb partial that includes either _user_navigation.html.erb or _admin_navigation.html.erb based on admin_signed_in? . Your view includes `<%= render "navigation" %>", and doesn't worry about the internal details.

If you have enough code that the above starts to feel unwieldy, then you should look at Cells for Rails . It's a gem that makes it easier to build encapsulated display components - combinations of controller logic and view code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM