layouts.rb 14.5 KB
Newer Older
1
module AbstractController
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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
  # 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 are only mentioned in one place, like this:
  #
  #   // The header part of this layout
  #   <%= yield %>
  #   // The footer part of this layout
  #
  # And then you have content pages that look like this:
  #
  #    hello world
  #
  # 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>
  #   <%= yield %>
  #
  # ...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
  #
  # == Layout assignment
  #
  # You can either specify a layout declaratively (using the #layout class method) or give
  # it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
  # If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
  #
  # For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
  # that template will be used for all actions in PostsController and controllers inheriting
  # from PostsController.
  #
  # If you use a module, for instance Weblog::PostsController, you will need a template named
  # <tt>app/views/layouts/weblog/posts.html.erb</tt>.
  #
  # Since all your controllers inherit from ApplicationController, they will use
  # <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
  # or provided.
  #
  # == Inheritance Examples
  #
  #   class BankController < ActionController::Base
  #     layout "bank_standard"
  #
  #   class InformationController < BankController
  #
  #   class TellerController < BankController
  #     # teller.html.erb exists
  #
  #   class TillController < TellerController
  #
  #   class VaultController < BankController
  #     layout :access_level_layout
  #
  #   class EmployeeController < BankController
  #     layout nil
  #
  # The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
  # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
  #
  # The TellerController uses +teller.html.erb+, and TillController inherits that layout and
  # uses it as well.
  #
  # == 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" }
  #
  # Of course, the most common way of specifying a layout is still just as a plain template name:
  #
  #   class WeblogController < ActionController::Base
  #     layout "weblog_standard"
  #
  # If no directory is specified for the template name, the template will by default be looked for in <tt>app/views/layouts/</tt>.
  # Otherwise, it will be looked up relative to the template root.
  #
  # == 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
  # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
  #
  #   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.
  #
  # 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>.
  #
  # == 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.
  # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
  # You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
  #
  #   class WeblogController < ActionController::Base
  #     layout "weblog_standard"
  #
  #     def help
  #       render :action => "help", :layout => "help"
  #     end
  #   end
  #
  # This will render the help action with the "help" layout instead of the controller-wide "weblog_standard" layout.
158
  module Layouts
159
    extend ActiveSupport::Concern
J
Joshua Peek 已提交
160

161
    include Rendering
J
Joshua Peek 已提交
162

163
    included do
J
Jeremy Kemper 已提交
164 165 166
      class_attribute :_layout_conditions
      delegate :_layout_conditions, :to => :'self.class'
      self._layout_conditions = {}
167
      _write_layout_method
168 169
    end

Y
Yehuda Katz and Carl Lerche 已提交
170
    module ClassMethods
171 172
      def inherited(klass)
        super
173
        klass._write_layout_method
174 175
      end

Y
Yehuda Katz 已提交
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
      # This module is mixed in if layout conditions are provided. This means
      # that if no layout conditions are used, this method is not used
      module LayoutConditions
        # Determines whether the current action has a layout by checking the
        # action name against the :only and :except conditions set on the
        # layout.
        #
        # ==== Returns
        # Boolean:: True if the action has a layout, false otherwise.
        def _action_has_layout?
          conditions = _layout_conditions

          if only = conditions[:only]
            only.include?(action_name)
          elsif except = conditions[:except]
            !except.include?(action_name)
          else
            true
          end
        end
      end

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
      # Specify the layout to use for this class.
      #
      # If the specified layout is a:
      # String:: the String is the template name
      # Symbol:: call the method specified by the symbol, which will return
      #   the template name
      # false::  There is no layout
      # true::   raise an ArgumentError
      #
      # ==== Parameters
      # layout<String, Symbol, false)>:: The layout to use.
      #
      # ==== Options (conditions)
      # :only<#to_s, Array[#to_s]>:: A list of actions to apply this layout to.
      # :except<#to_s, Array[#to_s]>:: Apply this layout to all actions but this one
213
      def layout(layout, conditions = {})
Y
Yehuda Katz 已提交
214 215
        include LayoutConditions unless conditions.empty?

216 217
        conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
        self._layout_conditions = conditions
J
Joshua Peek 已提交
218

219 220
        @_layout = layout || false # Converts nil to false
        _write_layout_method
221
      end
J
Joshua Peek 已提交
222

223 224 225 226 227
      # If no layout is supplied, look for a template named the return
      # value of this method.
      #
      # ==== Returns
      # String:: A template name
228
      def _implied_layout_name
229
        controller_path
230
      end
J
Joshua Peek 已提交
231

232 233
      # Takes the specified layout and creates a _layout method to be called
      # by _default_layout
J
Joshua Peek 已提交
234
      #
235 236 237 238
      # If there is no explicit layout specified:
      # If a layout is found in the view paths with the controller's
      # name, return that string. Otherwise, use the superclass'
      # layout (which might also be implied)
239
      def _write_layout_method
240
        case defined?(@_layout) ? @_layout : nil
241
        when String
242
          self.class_eval %{def _layout(details) #{@_layout.inspect} end}
243
        when Symbol
244 245 246 247 248 249 250 251 252 253
          self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
            def _layout(details)
              #{@_layout}.tap do |layout|
                unless layout.is_a?(String) || !layout
                  raise ArgumentError, "Your layout method :#{@_layout} returned \#{layout}. It " \
                    "should have returned a String, false, or nil"
                end
              end
            end
          ruby_eval
254 255 256
        when Proc
          define_method :_layout_from_proc, &@_layout
          self.class_eval %{def _layout(details) _layout_from_proc(self) end}
257
        when false
258
          self.class_eval %{def _layout(details) end}
259 260 261
        when true
          raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil"
        when nil
262 263 264
          if name
            self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def _layout(details)
265 266 267 268
                if template_exists?("#{_implied_layout_name}", :_prefix => "layouts")
                  "#{_implied_layout_name}"
                else
                  super
269
                end
270
              end
271 272
            RUBY
          end
273
        end
274
        self.class_eval { private :_layout }
275
      end
Y
Yehuda Katz and Carl Lerche 已提交
276
    end
J
Joshua Peek 已提交
277

278
    def render_to_body(options = {})
279 280 281 282
      # In the case of a partial with a layout, handle the layout
      # here, and make sure the view does not try to handle it
      layout = options.delete(:layout) if options.key?(:partial)

283 284
      response = super

285 286 287
      # This is a little bit messy. We need to explicitly handle partial
      # layouts here since the core lookup logic is in the view, but
      # we need to determine the layout based on the controller
288 289 290
      #
      # TODO: An easier way to handle this would probably be to override
      # render_template
291 292 293
      if layout
        layout = _layout_for_option(layout, options[:_template].details)
        response = layout.render(view_context, options[:locals] || {}) { response }
294 295 296 297 298
      end

      response
    end

Y
Yehuda Katz and Carl Lerche 已提交
299
  private
300

301 302
    # This will be overwritten by _write_layout_method
    def _layout(details) end
J
Joshua Peek 已提交
303

304 305 306 307 308 309 310
    # Determine the layout for a given name and details.
    #
    # ==== Parameters
    # name<String>:: The name of the template
    # details<Hash{Symbol => Object}>:: A list of details to restrict
    #   the lookup to. By default, layout lookup is limited to the
    #   formats specified for the current request.
311
    def _layout_for_name(name, details)
312
      name && _find_layout(name, details)
313 314
    end

315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
    # Determine the layout for a given name and details, taking into account
    # the name type.
    #
    # ==== Parameters
    # name<String|TrueClass|FalseClass|Symbol>:: The name of the template
    # details<Hash{Symbol => Object}>:: A list of details to restrict
    #   the lookup to. By default, layout lookup is limited to the
    #   formats specified for the current request.
    def _layout_for_option(name, details)
      case name
      when String     then _layout_for_name(name, details)
      when true       then _default_layout(details, true)
      when :default   then _default_layout(details, false)
      when false, nil then nil
      else
        raise ArgumentError,
          "String, true, or false, expected for `layout'; you passed #{name.inspect}"
      end
    end

335 336 337
    def _determine_template(options)
      super

338
      return unless (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
339 340 341 342
      layout = options.key?(:layout) ? options[:layout] : :default
      options[:_layout] = _layout_for_option(layout, options[:_template].details)
    end

343 344 345 346 347 348 349 350 351
    # Take in the name and details and find a Template.
    #
    # ==== Parameters
    # name<String>:: The name of the template to retrieve
    # details<Hash>:: A list of details to restrict the search by. This
    #   might include details like the format or locale of the template.
    #
    # ==== Returns
    # Template:: A template object matching the name and details
352
    def _find_layout(name, details)
353
      prefix = details.key?(:prefix) ? details.delete(:prefix) : "layouts"
354 355 356
      # TODO This should happen automatically
      template_lookup.details = template_lookup.details.merge(:formats => details[:formats])
      find_template(name, :_prefix => prefix)
Y
Yehuda Katz and Carl Lerche 已提交
357
    end
J
Joshua Peek 已提交
358

Y
Yehuda Katz 已提交
359
    # Returns the default layout for this controller and a given set of details.
360 361 362 363 364 365 366 367 368 369 370 371
    # Optionally raises an exception if the layout could not be found.
    #
    # ==== Parameters
    # details<Hash>:: A list of details to restrict the search by. This
    #   might include details like the format or locale of the template.
    # require_layout<Boolean>:: If this is true, raise an ArgumentError
    #   with details about the fact that the exception could not be
    #   found (defaults to false)
    #
    # ==== Returns
    # Template:: The template object for the default layout (or nil)
    def _default_layout(details, require_layout = false)
372
      if require_layout && _action_has_layout? && !_layout(details)
Y
Yehuda Katz + Carl Lerche 已提交
373
        raise ArgumentError,
374 375
          "There was no default layout for #{self.class} in #{view_paths.inspect}"
      end
Y
Yehuda Katz + Carl Lerche 已提交
376

377
      begin
378
        _layout_for_name(_layout(details), details) if _action_has_layout?
379
      rescue NameError => e
J
Joshua Peek 已提交
380
        raise NoMethodError,
381 382
          "You specified #{@_layout.inspect} as the layout, but no such method was found"
      end
Y
Yehuda Katz and Carl Lerche 已提交
383
    end
384 385

    def _action_has_layout?
Y
Yehuda Katz 已提交
386
      true
387
    end
388
  end
J
Joshua Peek 已提交
389
end