aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/abstract_controller/helpers.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/abstract_controller/helpers.rb')
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb194
1 files changed, 194 insertions, 0 deletions
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
new file mode 100644
index 0000000000..3913259ecc
--- /dev/null
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+require "active_support/dependencies"
+
+module AbstractController
+ module Helpers
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_helpers, default: Module.new
+ class_attribute :_helper_methods, default: Array.new
+ end
+
+ class MissingHelperError < LoadError
+ def initialize(error, path)
+ @error = error
+ @path = "helpers/#{path}.rb"
+ set_backtrace error.backtrace
+
+ if /^#{path}(\.rb)?$/.match?(error.path)
+ super("Missing helper file helpers/%s.rb" % path)
+ else
+ raise error
+ end
+ end
+ end
+
+ module ClassMethods
+ # When a class is inherited, wrap its helper module in a new module.
+ # This ensures that the parent class's module can be changed
+ # independently of the child class's.
+ def inherited(klass)
+ helpers = _helpers
+ klass._helpers = Module.new { include helpers }
+ klass.class_eval { default_helper_module! } unless klass.anonymous?
+ super
+ end
+
+ # Declare a controller method as a helper. For example, the following
+ # makes the +current_user+ and +logged_in?+ controller methods available
+ # to the view:
+ # class ApplicationController < ActionController::Base
+ # helper_method :current_user, :logged_in?
+ #
+ # def current_user
+ # @current_user ||= User.find_by(id: session[:user])
+ # end
+ #
+ # def logged_in?
+ # current_user != nil
+ # end
+ # end
+ #
+ # In a view:
+ # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
+ #
+ # ==== Parameters
+ # * <tt>method[, method]</tt> - A name or names of a method on the controller
+ # to be made available on the view.
+ def helper_method(*meths)
+ meths.flatten!
+ self._helper_methods += meths
+
+ meths.each do |meth|
+ _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
+ def #{meth}(*args, &blk) # def current_user(*args, &blk)
+ controller.send(%(#{meth}), *args, &blk) # controller.send(:current_user, *args, &blk)
+ end # end
+ ruby_eval
+ end
+ end
+
+ # The +helper+ class method can take a series of helper module names, a block, or both.
+ #
+ # ==== Options
+ # * <tt>*args</tt> - Module, Symbol, String
+ # * <tt>block</tt> - A block defining helper methods
+ #
+ # When the argument is a module it will be included directly in the template class.
+ # helper FooHelper # => includes FooHelper
+ #
+ # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file
+ # and include the module in the template class. The second form illustrates how to include custom helpers
+ # when working with namespaced controllers, or other cases where the file containing the helper definition is not
+ # in one of Rails' standard load paths:
+ # helper :foo # => requires 'foo_helper' and includes FooHelper
+ # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper
+ #
+ # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
+ # to the template.
+ #
+ # # One line
+ # helper { def hello() "Hello, world!" end }
+ #
+ # # Multi-line
+ # helper do
+ # def foo(bar)
+ # "#{bar} is the very best"
+ # end
+ # end
+ #
+ # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of
+ # +symbols+, +strings+, +modules+ and blocks.
+ #
+ # helper(:three, BlindHelper) { def mice() 'mice' end }
+ #
+ def helper(*args, &block)
+ modules_for_helpers(args).each do |mod|
+ add_template_helper(mod)
+ end
+
+ _helpers.module_eval(&block) if block_given?
+ end
+
+ # Clears up all existing helpers in this class, only keeping the helper
+ # with the same name as this class.
+ def clear_helpers
+ inherited_helper_methods = _helper_methods
+ self._helpers = Module.new
+ self._helper_methods = Array.new
+
+ inherited_helper_methods.each { |meth| helper_method meth }
+ default_helper_module! unless anonymous?
+ end
+
+ # Returns a list of modules, normalized from the acceptable kinds of
+ # helpers with the following behavior:
+ #
+ # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper",
+ # and "foo_bar_helper.rb" is loaded using require_dependency.
+ #
+ # Module:: No further processing
+ #
+ # After loading the appropriate files, the corresponding modules
+ # are returned.
+ #
+ # ==== Parameters
+ # * <tt>args</tt> - An array of helpers
+ #
+ # ==== Returns
+ # * <tt>Array</tt> - A normalized list of modules for the list of
+ # helpers provided.
+ def modules_for_helpers(args)
+ args.flatten.map! do |arg|
+ case arg
+ when String, Symbol
+ file_name = "#{arg.to_s.underscore}_helper"
+ begin
+ require_dependency(file_name)
+ rescue LoadError => e
+ raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
+ end
+
+ mod_name = file_name.camelize
+ begin
+ mod_name.constantize
+ rescue LoadError
+ # dependencies.rb gives a similar error message but its wording is
+ # not as clear because it mentions autoloading. To the user all it
+ # matters is that a helper module couldn't be loaded, autoloading
+ # is an internal mechanism that should not leak.
+ raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
+ end
+ when Module
+ arg
+ else
+ raise ArgumentError, "helper must be a String, Symbol, or Module"
+ end
+ end
+ end
+
+ private
+ # Makes all the (instance) methods in the helper module available to templates
+ # rendered through this controller.
+ #
+ # ==== Parameters
+ # * <tt>module</tt> - The module to include into the current helper module
+ # for the class
+ def add_template_helper(mod)
+ _helpers.module_eval { include mod }
+ end
+
+ def default_helper_module!
+ module_name = name.sub(/Controller$/, "")
+ module_path = module_name.underscore
+ helper module_path
+ rescue LoadError => e
+ raise e unless e.is_missing? "helpers/#{module_path}_helper"
+ rescue NameError => e
+ raise e unless e.missing_name? "#{module_name}Helper"
+ end
+ end
+ end
+end