helpers.rb 8.2 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2
module ActionController #:nodoc:
  module Helpers #:nodoc:
3
    HELPERS_DIR = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers")
J
Jeremy Kemper 已提交
4

5
    def self.included(base)
6 7 8 9 10
      # Initialize the base module to aggregate its helpers.
      base.class_inheritable_accessor :master_helper_module
      base.master_helper_module = Module.new

      # Extend base with class methods to declare helpers.
D
Initial  
David Heinemeier Hansson 已提交
11
      base.extend(ClassMethods)
12 13 14 15

      base.class_eval do
        # Wrap inherited to create a new master helper module for subclasses.
        class << self
16
          alias_method_chain :inherited, :helper
17 18
        end
      end
D
Initial  
David Heinemeier Hansson 已提交
19 20
    end

21
    # The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+, 
22
    # +numbers+ and +ActiveRecord+ objects, to name a few. These helpers are available to all templates
23
    # by default.
D
Initial  
David Heinemeier Hansson 已提交
24
    #
25 26 27
    # In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to
    # extract complicated logic or reusable functionality is strongly encouraged.  By default, the controller will 
    # include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically
28
    # include <tt>MyHelper</tt>.
29 30
    # 
    # Additional helpers can be specified using the +helper+ class method in <tt>ActionController::Base</tt> or any
31
    # controller which inherits from it.
J
Jeremy Kemper 已提交
32
    #
33 34 35 36 37 38 39 40
    # ==== Examples
    # The +to_s+ method from the +Time+ class can be wrapped in a helper method to display a custom message if 
    # the Time object is blank:
    #
    #   module FormattedTimeHelper
    #     def format_time(time, format=:long, blank_message="&nbsp;")
    #       time.blank? ? blank_message : time.to_s(format)
    #     end
D
Initial  
David Heinemeier Hansson 已提交
41
    #   end
J
Jeremy Kemper 已提交
42
    #
43
    # +FormattedTimeHelper+ can now be included in a controller, using the +helper+ class method:
J
Jeremy Kemper 已提交
44
    #
45 46 47 48 49
    #   class EventsController < ActionController::Base
    #     helper FormattedTimeHelper
    #     def index
    #       @events = Event.find(:all)
    #     end
D
Initial  
David Heinemeier Hansson 已提交
50
    #   end
J
Jeremy Kemper 已提交
51
    #
52 53 54 55 56 57 58 59 60 61 62 63 64
    # Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
    #
    #   <% @events.each do |event| -%>
    #     <p>
    #       <% format_time(event.time, :short, "N/A") %> | <%= event.name %> 
    #     </p>
    #   <% end -%>
    #
    # Finally, assuming we have two event instances, one which has a time and one which does not, 
    # the output might look like this:
    #
    #   23 Aug 11:30 | Carolina Railhawks Soccer Match 
    #   N/A | Carolina Railhaws Training Workshop
J
Jeremy Kemper 已提交
65
    #
D
Initial  
David Heinemeier Hansson 已提交
66 67
    module ClassMethods
      # Makes all the (instance) methods in the helper module available to templates rendered through this controller.
J
Jeremy Kemper 已提交
68
      # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
D
Initial  
David Heinemeier Hansson 已提交
69
      # available to the templates.
D
David Heinemeier Hansson 已提交
70
      def add_template_helper(helper_module) #:nodoc:
71
        master_helper_module.send(:include, helper_module)
D
Initial  
David Heinemeier Hansson 已提交
72 73
      end

74
      # The +helper+ class method can take a series of helper module names, a block, or both.
75
      #
76 77 78 79 80 81 82 83 84 85
      # * <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
86
      #
87 88
      # When the argument is a +Module+, it will be included directly in the template class.
      #   helper FooHelper # => includes FooHelper
89
      #
90
      # When the argument is the symbol <tt>:all</tt>, the controller will includes all helpers from 
91
      # <tt>app/views/helpers/**/*.rb</tt> under +RAILS_ROOT+.
92
      #   helper :all
93
      #
94 95 96 97 98 99 100 101 102 103 104
      # 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
      # 
105
      # Finally, all the above styles can be mixed together, and the +helper+ method can be invokved with a mix of
106
      # +symbols+, +strings+, +modules+ and blocks.
D
Initial  
David Heinemeier Hansson 已提交
107
      #   helper(:three, BlindHelper) { def mice() 'mice' end }
108
      #
D
Initial  
David Heinemeier Hansson 已提交
109 110 111
      def helper(*args, &block)
        args.flatten.each do |arg|
          case arg
112 113
            when Module
              add_template_helper(arg)
114 115
            when :all
              helper(all_application_helpers)
116 117 118
            when String, Symbol
              file_name  = arg.to_s.underscore + '_helper'
              class_name = file_name.camelize
J
Jeremy Kemper 已提交
119

120 121 122 123
              begin
                require_dependency(file_name)
              rescue LoadError => load_error
                requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
124 125 126 127 128 129
                if requiree == file_name
                  msg = "Missing helper file helpers/#{file_name}.rb"
                  raise LoadError.new(msg).copy_blame!(load_error)
                else
                  raise
                end
130
              end
131 132 133

              add_template_helper(class_name.constantize)
            else
134
              raise ArgumentError, "helper expects String, Symbol, or Module argument (was: #{args.inspect})"
D
Initial  
David Heinemeier Hansson 已提交
135 136 137 138
          end
        end

        # Evaluate block in template class if given.
139
        master_helper_module.module_eval(&block) if block_given?
D
Initial  
David Heinemeier Hansson 已提交
140
      end
J
Jeremy Kemper 已提交
141

142 143
      # Declare a controller method as a helper. For example, the following
      # makes the +current_user+ controller method available to the view:
144 145 146 147 148 149
      #   class ApplicationController < ActionController::Base
      #     helper_method :current_user
      #     def current_user
      #       @current_user ||= User.find(session[:user])
      #     end
      #   end
D
Initial  
David Heinemeier Hansson 已提交
150
      def helper_method(*methods)
151 152 153 154 155 156 157
        methods.flatten.each do |method|
          master_helper_module.module_eval <<-end_eval
            def #{method}(*args, &block)
              controller.send(%(#{method}), *args, &block)
            end
          end_eval
        end
D
Initial  
David Heinemeier Hansson 已提交
158 159
      end

160 161 162
      # 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:
D
Initial  
David Heinemeier Hansson 已提交
163 164 165 166 167 168
      #   helper_attr :name
      #   attr_accessor :name
      def helper_attr(*attrs)
        attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
      end

169

J
Jeremy Kemper 已提交
170
      private
171
        def default_helper_module!
172
          module_name = name.sub(/Controller$|$/, 'Helper')
173 174 175
          module_path = module_name.split('::').map { |m| m.underscore }.join('/')
          require_dependency module_path
          helper module_name.constantize
176 177
        rescue LoadError => e
          raise unless e.is_missing? module_path
178
          logger.debug("#{name}: missing default helper path #{module_path}") if logger
179 180
        rescue NameError => e
          raise unless e.missing_name? module_name
181 182 183
          logger.debug("#{name}: missing default helper module #{module_name}") if logger
        end

184
        def inherited_with_helper(child)
D
Initial  
David Heinemeier Hansson 已提交
185
          inherited_without_helper(child)
186

187 188 189
          begin
            child.master_helper_module = Module.new
            child.master_helper_module.send :include, master_helper_module
190
            child.send :default_helper_module!
191
          rescue MissingSourceFile => e
192
            raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
D
Initial  
David Heinemeier Hansson 已提交
193
          end
194
        end
J
Jeremy Kemper 已提交
195 196

        # Extract helper names from files in app/helpers/**/*.rb
197
        def all_application_helpers
J
Jeremy Kemper 已提交
198 199
          extract = /^#{Regexp.quote(HELPERS_DIR)}\/?(.*)_helper.rb$/
          Dir["#{HELPERS_DIR}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
200
        end
D
Initial  
David Heinemeier Hansson 已提交
201 202
    end
  end
J
Jeremy Kemper 已提交
203
end