# frozen_string_literal: true require "active_support/dependencies" module AbstractController module Helpers extend ActiveSupport::Concern included do class_attribute :_helpers, default: define_helpers_module(self) 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 = define_helpers_module(klass, 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 # * method[, method] - 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 location = caller_locations(1, 1).first file, line = location.path, location.lineno meths.each do |meth| method_def = [ "def #{meth}(*args, &blk)", " controller.send(%(#{meth}), *args, &blk)", "end" ].join(";") _helpers.class_eval method_def, file, line end end # The +helper+ class method can take a series of helper module names, a block, or both. # # ==== Options # * *args - Module, Symbol, String # * block - 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 # * args - An array of helpers # # ==== Returns # * Array - 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 def define_helpers_module(klass, helpers = nil) # In some tests inherited is called explicitly. In that case, just # return the module from the first time it was defined return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false) mod = Module.new klass.const_set(:HelperMethods, mod) mod.include(helpers) if helpers mod end # Makes all the (instance) methods in the helper module available to templates # rendered through this controller. # # ==== Parameters # * module - 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