layouts.rb 12.8 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
J
José Valim 已提交
242
          self.class_eval %{def _layout; #{@_layout.inspect} end}
243
        when Symbol
244
          self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
J
José Valim 已提交
245
            def _layout
246 247 248 249 250 251 252 253
              #{@_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
        when Proc
          define_method :_layout_from_proc, &@_layout
J
José Valim 已提交
256
          self.class_eval %{def _layout; _layout_from_proc(self) end}
257
        when false
J
José Valim 已提交
258
          self.class_eval %{def _layout; end}
259 260 261
        when true
          raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil"
        when nil
262
          if name
J
José Valim 已提交
263 264
            _prefix = "layouts" unless _implied_layout_name =~ /\blayouts/

265
            self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
J
José Valim 已提交
266
              def _layout
267
                if template_exists?("#{_implied_layout_name}", #{_prefix.inspect})
268 269 270
                  "#{_implied_layout_name}"
                else
                  super
271
                end
272
              end
273 274
            RUBY
          end
275
        end
276
        self.class_eval { private :_layout }
277
      end
Y
Yehuda Katz and Carl Lerche 已提交
278
    end
J
Joshua Peek 已提交
279

280 281 282
    def _normalize_options(options)
      super

283
      if _include_layout?(options)
284
        layout = options.key?(:layout) ? options.delete(:layout) : :default
J
José Valim 已提交
285
        value = _layout_for_option(layout)
286
        options[:layout] = (value =~ /\blayouts/ ? value : "layouts/#{value}") if value
287 288 289
      end
    end

Y
Yehuda Katz and Carl Lerche 已提交
290
  private
291

292
    # This will be overwritten by _write_layout_method
J
José Valim 已提交
293
    def _layout; end
294

295 296 297 298 299 300 301 302
    # 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.
J
José Valim 已提交
303
    def _layout_for_option(name)
304
      case name
J
José Valim 已提交
305 306 307
      when String     then name
      when true       then _default_layout(true)
      when :default   then _default_layout(false)
308 309 310 311 312 313 314
      when false, nil then nil
      else
        raise ArgumentError,
          "String, true, or false, expected for `layout'; you passed #{name.inspect}"
      end
    end

Y
Yehuda Katz 已提交
315
    # Returns the default layout for this controller and a given set of details.
316 317 318 319 320 321 322 323 324 325 326
    # 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)
J
José Valim 已提交
327
    def _default_layout(require_layout = false)
328
      begin
J
José Valim 已提交
329
        layout_name = _layout if _action_has_layout?
330
      rescue NameError => e
J
Joshua Peek 已提交
331
        raise NoMethodError,
332 333
          "You specified #{@_layout.inspect} as the layout, but no such method was found"
      end
J
José Valim 已提交
334 335 336 337 338 339 340

      if require_layout && _action_has_layout? && !layout_name
        raise ArgumentError,
          "There was no default layout for #{self.class} in #{view_paths.inspect}"
      end

      layout_name
Y
Yehuda Katz and Carl Lerche 已提交
341
    end
342

343 344 345 346
    def _include_layout?(options)
      (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
    end

347
    def _action_has_layout?
Y
Yehuda Katz 已提交
348
      true
349
    end
350
  end
J
Joshua Peek 已提交
351
end