layout.rb 11.0 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2 3 4 5
module ActionController #:nodoc:
  module Layout #:nodoc:
    def self.append_features(base)
      super
      base.class_eval do
6 7
        alias_method :render_with_no_layout, :render
        alias_method :render, :render_with_a_layout
8

9 10 11
        class << self
          alias_method :inherited_without_layout, :inherited
        end
D
Initial  
David Heinemeier Hansson 已提交
12
      end
13
      base.extend(ClassMethods)
D
Initial  
David Heinemeier Hansson 已提交
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
    end

    # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
    # repeated setups. The inclusion pattern has pages that look like this:
    #
    #   <%= render "shared/header" %>
    #   Hello World
    #   <%= render "shared/footer" %>
    #
    # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
    # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
    #
    # With layouts, you can flip it around and have the common structure know where to insert changing content. This means
    # that the header and footer is only mentioned in one place, like this:
    #
    #   <!-- The header part of this layout -->
    #   <%= @content_for_layout %>
    #   <!-- The footer part of this layout -->
    #
    # And then you have content pages that look like this:
    #
    #    hello world
    #
    # Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout, 
    # like this:
    #
    #   <!-- The header part of this layout -->
    #   hello world
    #   <!-- The footer part of this layout -->
    #
    # == Accessing shared variables
    #
    # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
    # references that won't materialize before rendering time:
    #
    #   <h1><%= @page_title %></h1>
    #   <%= @content_for_layout %>
    #
    # ...and content pages that fulfill these references _at_ rendering time:
    #
    #    <% @page_title = "Welcome" %>
    #    Off-world colonies offers you a chance to start a new life
    #
    # The result after rendering is:
    #
    #   <h1>Welcome</h1>
    #   Off-world colonies offers you a chance to start a new life
    #
62 63 64 65 66 67 68
    # == Automatic layout assignment
    #
    # If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically
    # set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named 
    # <tt>app/views/layouts/weblog.rhtml</tt> or <tt>app/views/layouts/weblog.rxml</tt> exists then it will be automatically set as
    # the layout for your WeblogController. You can create a layout with the name <tt>application.rhtml</tt> or <tt>application.rxml</tt>
    # and this will be set as the default controller if there is no layout with the same name as the current controller and there is 
69
    # no layout explicitly assigned with the +layout+ method. Setting a layout explicitly will always override the automatic behaviour. 
70
    #
D
Initial  
David Heinemeier Hansson 已提交
71 72 73 74 75
    # == Inheritance for layouts
    #
    # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
    #
    #   class BankController < ActionController::Base
76
    #     layout "bank_standard"
D
Initial  
David Heinemeier Hansson 已提交
77 78 79 80 81 82 83 84 85
    #
    #   class InformationController < BankController
    #
    #   class VaultController < BankController
    #     layout :access_level_layout
    #
    #   class EmployeeController < BankController
    #     layout nil
    #
86
    # The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
D
Initial  
David Heinemeier Hansson 已提交
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
    # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
    #
    # == Types of layouts
    #
    # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
    # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
    # be done either by specifying a method reference as a symbol or using an inline method (as a proc).
    #
    # The method reference is the preferred approach to variable layouts and is used like this:
    #
    #   class WeblogController < ActionController::Base
    #     layout :writers_and_readers
    #
    #     def index
    #       # fetching posts
    #     end
    #
    #     private
    #       def writers_and_readers
    #         logged_in? ? "writer_layout" : "reader_layout"
    #       end
    #
    # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing 
    # is logged in or not.
    #
    # If you want to use an inline method, such as a proc, do something like this:
    #
    #   class WeblogController < ActionController::Base
    #     layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
    #
117
    # Of course, the most common way of specifying a layout is still just as a plain template name:
D
Initial  
David Heinemeier Hansson 已提交
118 119
    #
    #   class WeblogController < ActionController::Base
120
    #     layout "weblog_standard"
D
Initial  
David Heinemeier Hansson 已提交
121
    #
122
    # If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
D
Initial  
David Heinemeier Hansson 已提交
123
    #
124 125 126 127
    # == Conditional layouts
    #
    # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
    # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The 
128
    # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
129 130 131 132 133 134 135 136 137 138 139
    #
    #   class WeblogController < ActionController::Base
    #     layout "weblog_standard", :except => :rss
    # 
    #     # ...
    #
    #   end
    #
    # This will assign "weblog_standard" as the WeblogController's layout  except for the +rss+ action, which will not wrap a layout 
    # around the rendered view.
    #
140 141
    # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so 
    # #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
142
    #
143 144 145 146
    # == Using a different layout in the action render call
    # 
    # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
    # Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
147
    # This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
148 149 150 151
    # qualified template and layout names as this example shows:
    #
    #   class WeblogController < ActionController::Base
    #     def help
152
    #       render :action => "help/index", :layout => "help"
153 154 155 156 157
    #     end
    #   end
    #
    # As you can see, you pass the template as the first parameter, the status code as the second ("200" is OK), and the layout
    # as the third.
D
Initial  
David Heinemeier Hansson 已提交
158 159 160 161 162
    module ClassMethods
      # If a layout is specified, all actions rendered through render and render_action will have their result assigned 
      # to <tt>@content_for_layout</tt>, which can then be used by the layout to insert their contents with
      # <tt><%= @content_for_layout %></tt>. This layout can itself depend on instance variables assigned during action
      # performance and have access to them as any normal template would.
163 164
      def layout(template_name, conditions = {})
        add_layout_conditions(conditions)
D
Initial  
David Heinemeier Hansson 已提交
165 166
        write_inheritable_attribute "layout", template_name
      end
167

168 169 170 171
      def layout_conditions #:nodoc:
        read_inheritable_attribute("layout_conditions")
      end

172 173 174
      private
        def inherited(child)
          inherited_without_layout(child)
175
          child.layout(child.controller_name) unless layout_list.grep(/^#{child.controller_name}\.r(?:x|ht)ml$/).empty?
176 177 178
        end

        def layout_list
179 180 181 182 183 184 185 186 187
          Dir.glob("#{template_root}/layouts/*.r{x,ht}ml").map { |layout| File.basename(layout) }
        end

        def add_layout_conditions(conditions)
          write_inheritable_hash "layout_conditions", normalize_conditions(conditions)
        end

        def normalize_conditions(conditions)
          conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
188
        end
D
Initial  
David Heinemeier Hansson 已提交
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    end

    # Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
    # is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
    # object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
    # weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
    def active_layout(passed_layout = nil)
      layout = passed_layout || self.class.read_inheritable_attribute("layout")
      active_layout = case layout
        when Symbol then send(layout)
        when Proc   then layout.call(self)
        when String then layout
      end
      active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout
    end

205
    def render_with_a_layout(options = {}, deprecated_status = nil, deprecated_layout = nil) #:nodoc:
206 207
      options = render_with_a_layout_options(options)
      if (layout = pick_layout(options, deprecated_layout))
208
        logger.info("Rendering #{options[:template]} within #{layout}") unless logger.nil?
209

210 211
        @content_for_layout = render_with_no_layout(options.merge(:layout => false))
        erase_render_results
212 213

        add_variables_to_assigns
214
        render_with_no_layout(options.merge({ :text => @template.render_file(layout, true), :status => options[:status] || deprecated_status }))
215
      else
216
        render_with_no_layout(options, deprecated_status)
217 218 219
      end
    end

220
    private
221
      def render_with_a_layout_options(options)
222 223
        return { :template => options } unless options.is_a?(Hash)
        if options.values_at(:text, :file, :inline, :partial, :nothing).compact.empty?
224
          options
225 226
        else
          { :layout => false }.merge(options)
227 228 229
        end
      end

230
      def pick_layout(options = {}, deprecated_layout = nil)
231
        return deprecated_layout if !deprecated_layout.nil?
232

233 234 235 236
        if options.is_a?(Hash)
          case options[:layout]
            when FalseClass
              nil
237
            when NilClass, TrueClass
238 239 240 241 242 243
              active_layout if action_has_layout?
            else
              active_layout(options[:layout])
          end
        else
          (deprecated_layout || active_layout) if action_has_layout?
244 245 246
        end
      end
    
247
      def action_has_layout?
248
        conditions = self.class.layout_conditions || {}
249 250 251 252 253 254 255 256 257
        case
          when conditions[:only]
            conditions[:only].include?(action_name)
          when conditions[:except]
            !conditions[:except].include?(action_name) 
          else
            true
        end
      end
D
Initial  
David Heinemeier Hansson 已提交
258
  end
259
end