aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/new_base/helpers.rb
blob: 8925dd3969d28d364195df40c4e685be47928dc6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/name_error'
require 'active_support/dependencies'

module ActionController
  module Helpers
    extend ActiveSupport::Concern

    include AbstractController::Helpers

    included do
      # Set the default directory for helpers
      class_inheritable_accessor :helpers_dir
      self.helpers_dir = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers")
    end

    module ClassMethods
      def inherited(klass)
        klass.__send__ :default_helper_module!
        super
      end

      # The +helper+ class method can take a series of helper module names, a block, or both.
      #
      # * <tt>*args</tt>: One or more modules, strings or symbols, or the special symbol <tt>:all</tt>.
      # * <tt>&block</tt>: A block defining helper methods.
      #
      # ==== Examples
      # 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
      #
      # When the argument is a module it will be included directly in the template class.
      #   helper FooHelper # => includes FooHelper
      #
      # When the argument is the symbol <tt>:all</tt>, the controller will include all helpers beneath
      # <tt>ActionController::Base.helpers_dir</tt> (defaults to <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT).
      #   helper :all
      #
      # 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)
        args.flatten.each do |arg|
          case arg
          when :all
            helper all_application_helpers
          when String, Symbol
            file_name  = arg.to_s.underscore + '_helper'
            class_name = file_name.camelize

            begin
              require_dependency(file_name)
            rescue LoadError => load_error
              requiree = / -- (.*?)(\.rb)?$/.match(load_error.message).to_a[1]
              if requiree == file_name
                msg = "Missing helper file helpers/#{file_name}.rb"
                raise LoadError.new(msg).copy_blame!(load_error)
              else
                raise
              end
            end

            super class_name.constantize
          else
            super args
          end
        end

        # Evaluate block in template class if given.
        _helpers.module_eval(&block) if block_given?
      end

      # Declares helper accessors for controller attributes. For example, the
      # following adds new +name+ and <tt>name=</tt> instance methods to a
      # controller and makes them available to the view:
      #   helper_attr :name
      #   attr_accessor :name
      def helper_attr(*attrs)
        attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
      end

      # Provides a proxy to access helpers methods from outside the view.
      def helpers
        unless @helper_proxy
          @helper_proxy = ActionView::Base.new
          @helper_proxy.extend _helpers
        else
          @helper_proxy
        end
      end

      private
        def default_helper_module!
          unless name.blank?
            module_name = name.sub(/Controller$|$/, 'Helper')
            module_path = module_name.split('::').map { |m| m.underscore }.join('/')
            require_dependency module_path
            helper module_name.constantize
          end
        rescue MissingSourceFile => e
          raise unless e.is_missing? module_path
        rescue NameError => e
          raise unless e.missing_name? module_name
        end

        # Extract helper names from files in app/helpers/**/*.rb
        def all_application_helpers
          extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/
          Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
        end
    end
  end
end