reactive-mvc 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/Manifest +34 -0
- data/README +30 -0
- data/Rakefile +5 -0
- data/lib/reactive-mvc.rb +26 -0
- data/lib/reactive-mvc/controller.rb +16 -0
- data/lib/reactive-mvc/controller/base.rb +405 -0
- data/lib/reactive-mvc/controller/filters.rb +767 -0
- data/lib/reactive-mvc/controller/flash.rb +161 -0
- data/lib/reactive-mvc/controller/helpers.rb +203 -0
- data/lib/reactive-mvc/controller/layout.rb +285 -0
- data/lib/reactive-mvc/controller/output.rb +262 -0
- data/lib/reactive-mvc/controller/rescue.rb +208 -0
- data/lib/reactive-mvc/dispatcher.rb +133 -0
- data/lib/reactive-mvc/view.rb +18 -0
- data/lib/reactive-mvc/view/base.rb +388 -0
- data/lib/reactive-mvc/view/helpers.rb +38 -0
- data/lib/reactive-mvc/view/partials.rb +207 -0
- data/lib/reactive-mvc/view/paths.rb +125 -0
- data/lib/reactive-mvc/view/renderable.rb +98 -0
- data/lib/reactive-mvc/view/renderable_partial.rb +49 -0
- data/lib/reactive-mvc/view/template.rb +110 -0
- data/lib/reactive-mvc/view/template_error.rb +9 -0
- data/lib/reactive-mvc/view/template_handler.rb +9 -0
- data/lib/reactive-mvc/view/template_handlers.rb +43 -0
- data/lib/reactive-mvc/view/template_handlers/builder.rb +15 -0
- data/lib/reactive-mvc/view/template_handlers/erb.rb +20 -0
- data/lib/reactive-mvc/view/template_handlers/ruby_code.rb +9 -0
- data/reactive_app_generators/mvc/USAGE +10 -0
- data/reactive_app_generators/mvc/mvc_generator.rb +48 -0
- data/reactive_app_generators/mvc/templates/application_controller.rb +2 -0
- data/reactive_generators/controller/USAGE +7 -0
- data/reactive_generators/controller/controller_generator.rb +24 -0
- data/reactive_generators/controller/templates/controller.rb +2 -0
- metadata +113 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
module Reactive::Mvc::Controller #:nodoc:
|
2
|
+
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
|
3
|
+
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
|
4
|
+
# action that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can
|
5
|
+
# then expose the flash to its template. Actually, that exposure is automatically done. Example:
|
6
|
+
#
|
7
|
+
# class WeblogController < Reactive::Controller::Base
|
8
|
+
# def create
|
9
|
+
# # save post
|
10
|
+
# flash[:notice] = "Successfully created post"
|
11
|
+
# redirect_to :action => "display", :params => { :id => post.id }
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# def display
|
15
|
+
# # doesn't need to assign the flash notice to the template, that's done automatically
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# display.erb
|
20
|
+
# <% if flash[:notice] %><div class="notice"><%= flash[:notice] %></div><% end %>
|
21
|
+
#
|
22
|
+
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as
|
23
|
+
# many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
|
24
|
+
#
|
25
|
+
# See docs on the FlashHash class for more details about the flash.
|
26
|
+
module Flash
|
27
|
+
def self.included(base)
|
28
|
+
base.class_eval do
|
29
|
+
include InstanceMethods
|
30
|
+
alias_method_chain :assign_shortcuts, :flash
|
31
|
+
alias_method_chain :process_cleanup, :flash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
class FlashNow #:nodoc:
|
37
|
+
def initialize(flash)
|
38
|
+
@flash = flash
|
39
|
+
end
|
40
|
+
|
41
|
+
def []=(k, v)
|
42
|
+
@flash[k] = v
|
43
|
+
@flash.discard(k)
|
44
|
+
v
|
45
|
+
end
|
46
|
+
|
47
|
+
def [](k)
|
48
|
+
@flash[k]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class FlashHash < Hash
|
53
|
+
def initialize #:nodoc:
|
54
|
+
super
|
55
|
+
@used = {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def []=(k, v) #:nodoc:
|
59
|
+
keep(k)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def update(h) #:nodoc:
|
64
|
+
h.keys.each { |k| keep(k) }
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
alias :merge! :update
|
69
|
+
|
70
|
+
def replace(h) #:nodoc:
|
71
|
+
@used = {}
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
# Sets a flash that will not be available to the next action, only to the current.
|
76
|
+
#
|
77
|
+
# flash.now[:message] = "Hello current action"
|
78
|
+
#
|
79
|
+
# This method enables you to use the flash as a central messaging system in your app.
|
80
|
+
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
|
81
|
+
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
|
82
|
+
# vanish when the current action is done.
|
83
|
+
#
|
84
|
+
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
|
85
|
+
def now
|
86
|
+
FlashNow.new(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Keeps either the entire current flash or a specific flash entry available for the next action:
|
90
|
+
#
|
91
|
+
# flash.keep # keeps the entire flash
|
92
|
+
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
|
93
|
+
def keep(k = nil)
|
94
|
+
use(k, false)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
|
98
|
+
#
|
99
|
+
# flash.discard # discard the entire flash at the end of the current action
|
100
|
+
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
|
101
|
+
def discard(k = nil)
|
102
|
+
use(k)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Mark for removal entries that were kept, and delete unkept ones.
|
106
|
+
#
|
107
|
+
# This method is called automatically by filters, so you generally don't need to care about it.
|
108
|
+
def sweep #:nodoc:
|
109
|
+
keys.each do |k|
|
110
|
+
unless @used[k]
|
111
|
+
use(k)
|
112
|
+
else
|
113
|
+
delete(k)
|
114
|
+
@used.delete(k)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# clean up after keys that could have been left over by calling reject! or shift on the flash
|
119
|
+
(@used.keys - keys).each{ |k| @used.delete(k) }
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
|
124
|
+
# use() # marks the entire flash as used
|
125
|
+
# use('msg') # marks the "msg" entry as used
|
126
|
+
# use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
|
127
|
+
# use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
|
128
|
+
def use(k=nil, v=true)
|
129
|
+
unless k.nil?
|
130
|
+
@used[k] = v
|
131
|
+
else
|
132
|
+
keys.each{ |key| use(key, v) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
module InstanceMethods #:nodoc:
|
138
|
+
protected
|
139
|
+
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
|
140
|
+
# <tt>flash["notice"] = "hello"</tt> to put a new one.
|
141
|
+
# Note that if sessions are disabled only flash.now will work.
|
142
|
+
def flash(refresh = false) #:doc:
|
143
|
+
if !defined?(@_flash) || refresh
|
144
|
+
@_flash = FlashHash.new
|
145
|
+
end
|
146
|
+
@_flash
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def assign_shortcuts_with_flash(request, response) #:nodoc:
|
151
|
+
assign_shortcuts_without_flash(request, response)
|
152
|
+
flash(:refresh)
|
153
|
+
end
|
154
|
+
|
155
|
+
def process_cleanup_with_flash
|
156
|
+
flash.sweep if @_session
|
157
|
+
process_cleanup_without_flash
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# FIXME: helper { ... } is broken on Ruby 1.9
|
2
|
+
module Reactive::Mvc::Controller #:nodoc:
|
3
|
+
module Helpers #:nodoc:
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
# Initialize the base module to aggregate its helpers.
|
7
|
+
base.class_inheritable_accessor :master_helper_module
|
8
|
+
base.master_helper_module = Module.new
|
9
|
+
|
10
|
+
# Extend base with class methods to declare helpers.
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
|
13
|
+
base.class_eval do
|
14
|
+
# Wrap inherited to create a new master helper module for subclasses.
|
15
|
+
class << self
|
16
|
+
alias_method_chain :inherited, :helper
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
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.
|
24
|
+
#
|
25
|
+
# In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to
|
26
|
+
# extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will
|
27
|
+
# 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.
|
32
|
+
#
|
33
|
+
# ==== Examples
|
34
|
+
# The +to_s+ method from the +Time+ class can be wrapped in a helper method to display a custom message if
|
35
|
+
# the Time object is blank:
|
36
|
+
#
|
37
|
+
# module FormattedTimeHelper
|
38
|
+
# def format_time(time, format=:long, blank_message=" ")
|
39
|
+
# time.blank? ? blank_message : time.to_s(format)
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# +FormattedTimeHelper+ can now be included in a controller, using the +helper+ class method:
|
44
|
+
#
|
45
|
+
# class EventsController < ActionController::Base
|
46
|
+
# helper FormattedTimeHelper
|
47
|
+
# def index
|
48
|
+
# @events = Event.find(:all)
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
|
53
|
+
#
|
54
|
+
# <% @events.each do |event| -%>
|
55
|
+
# <p>
|
56
|
+
# <% format_time(event.time, :short, "N/A") %> | <%= event.name %>
|
57
|
+
# </p>
|
58
|
+
# <% end -%>
|
59
|
+
#
|
60
|
+
# Finally, assuming we have two event instances, one which has a time and one which does not,
|
61
|
+
# the output might look like this:
|
62
|
+
#
|
63
|
+
# 23 Aug 11:30 | Carolina Railhawks Soccer Match
|
64
|
+
# N/A | Carolina Railhaws Training Workshop
|
65
|
+
#
|
66
|
+
module ClassMethods
|
67
|
+
# Makes all the (instance) methods in the helper module available to templates rendered through this controller.
|
68
|
+
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
|
69
|
+
# available to the templates.
|
70
|
+
def add_template_helper(helper_module) #:nodoc:
|
71
|
+
master_helper_module.module_eval { include helper_module }
|
72
|
+
end
|
73
|
+
|
74
|
+
# The +helper+ class method can take a series of helper module names, a block, or both.
|
75
|
+
#
|
76
|
+
# * <tt>*args</tt>: One or more +Modules+, +Strings+ or +Symbols+, or the special symbol <tt>:all</tt>.
|
77
|
+
# * <tt>&block</tt>: A block defining helper methods.
|
78
|
+
#
|
79
|
+
# ==== Examples
|
80
|
+
# When the argument is a +String+ or +Symbol+, the method will provide the "_helper" suffix, require the file
|
81
|
+
# and include the module in the template class. The second form illustrates how to include custom helpers
|
82
|
+
# when working with namespaced controllers, or other cases where the file containing the helper definition is not
|
83
|
+
# in one of Rails' standard load paths:
|
84
|
+
# helper :foo # => requires 'foo_helper' and includes FooHelper
|
85
|
+
# helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper
|
86
|
+
#
|
87
|
+
# When the argument is a +Module+, it will be included directly in the template class.
|
88
|
+
# helper FooHelper # => includes FooHelper
|
89
|
+
#
|
90
|
+
# When the argument is the symbol <tt>:all</tt>, the controller will include all helpers from
|
91
|
+
# <tt>app/helpers/**/*.rb</tt> under +RAILS_ROOT+.
|
92
|
+
# helper :all
|
93
|
+
#
|
94
|
+
# Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
|
95
|
+
# to the template.
|
96
|
+
# # One line
|
97
|
+
# helper { def hello() "Hello, world!" end }
|
98
|
+
# # Multi-line
|
99
|
+
# helper do
|
100
|
+
# def foo(bar)
|
101
|
+
# "#{bar} is the very best"
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of
|
106
|
+
# +symbols+, +strings+, +modules+ and blocks.
|
107
|
+
# helper(:three, BlindHelper) { def mice() 'mice' end }
|
108
|
+
#
|
109
|
+
def helper(*args, &block)
|
110
|
+
args.flatten.each do |arg|
|
111
|
+
case arg
|
112
|
+
when Module
|
113
|
+
add_template_helper(arg)
|
114
|
+
when :all
|
115
|
+
helper(all_application_helpers)
|
116
|
+
when String, Symbol
|
117
|
+
file_name = arg.to_s.underscore + '_helper'
|
118
|
+
class_name = file_name.camelize
|
119
|
+
|
120
|
+
begin
|
121
|
+
require_dependency(file_name)
|
122
|
+
rescue LoadError => load_error
|
123
|
+
requiree = / -- (.*?)(\.rb)?$/.match(load_error.message).to_a[1]
|
124
|
+
if requiree == file_name
|
125
|
+
msg = "Missing helper file helpers/#{file_name}.rb"
|
126
|
+
raise LoadError.new(msg).copy_blame!(load_error)
|
127
|
+
else
|
128
|
+
raise
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
add_template_helper(class_name.constantize)
|
133
|
+
else
|
134
|
+
raise ArgumentError, "helper expects String, Symbol, or Module argument (was: #{args.inspect})"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Evaluate block in template class if given.
|
139
|
+
master_helper_module.module_eval(&block) if block_given?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Declare a controller method as a helper. For example, the following
|
143
|
+
# makes the +current_user+ controller method available to the view:
|
144
|
+
# class ApplicationController < ActionController::Base
|
145
|
+
# helper_method :current_user
|
146
|
+
# def current_user
|
147
|
+
# @current_user ||= User.find(session[:user])
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
def helper_method(*methods)
|
151
|
+
methods.flatten.each do |method|
|
152
|
+
master_helper_module.module_eval <<-end_eval
|
153
|
+
def #{method}(*args, &block)
|
154
|
+
controller.send(%(#{method}), *args, &block)
|
155
|
+
end
|
156
|
+
end_eval
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Declares helper accessors for controller attributes. For example, the
|
161
|
+
# following adds new +name+ and <tt>name=</tt> instance methods to a
|
162
|
+
# controller and makes them available to the view:
|
163
|
+
# helper_attr :name
|
164
|
+
# attr_accessor :name
|
165
|
+
def helper_attr(*attrs)
|
166
|
+
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
private
|
171
|
+
def default_helper_module!
|
172
|
+
unless name.blank?
|
173
|
+
module_name = name.sub(/Controller$|$/, 'Helper')
|
174
|
+
module_path = module_name.split('::').map { |m| m.underscore }.join('/')
|
175
|
+
require_dependency module_path
|
176
|
+
helper module_name.constantize
|
177
|
+
end
|
178
|
+
rescue MissingSourceFile => e
|
179
|
+
raise unless e.is_missing? module_path
|
180
|
+
rescue NameError => e
|
181
|
+
raise unless e.missing_name? module_name
|
182
|
+
end
|
183
|
+
|
184
|
+
def inherited_with_helper(child)
|
185
|
+
inherited_without_helper(child)
|
186
|
+
|
187
|
+
begin
|
188
|
+
child.master_helper_module = Module.new
|
189
|
+
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")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Extract helper names from files in app/helpers/**/*.rb
|
197
|
+
def all_application_helpers
|
198
|
+
extract = /^#{Regexp.quote(Reactive.dir_for(:helper))}\/?(.*)_helper.rb$/
|
199
|
+
Dir["#{Reactive.dir_for(:helper)}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
module Reactive::Mvc::Controller #:nodoc:
|
2
|
+
module Layout #:nodoc:
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
base.class_eval do
|
6
|
+
class << self
|
7
|
+
alias_method_chain :inherited, :layout
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
|
13
|
+
# repeated setups. The inclusion pattern has pages that look like this:
|
14
|
+
#
|
15
|
+
# <%= render "shared/header" %>
|
16
|
+
# Hello World
|
17
|
+
# <%= render "shared/footer" %>
|
18
|
+
#
|
19
|
+
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
|
20
|
+
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
|
21
|
+
#
|
22
|
+
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
|
23
|
+
# that the header and footer are only mentioned in one place, like this:
|
24
|
+
#
|
25
|
+
# // The header part of this layout
|
26
|
+
# <%= yield %>
|
27
|
+
# // The footer part of this layout -->
|
28
|
+
#
|
29
|
+
# And then you have content pages that look like this:
|
30
|
+
#
|
31
|
+
# hello world
|
32
|
+
#
|
33
|
+
# Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
|
34
|
+
# like this:
|
35
|
+
#
|
36
|
+
# // The header part of this layout
|
37
|
+
# hello world
|
38
|
+
# // The footer part of this layout -->
|
39
|
+
#
|
40
|
+
# == Accessing shared variables
|
41
|
+
#
|
42
|
+
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
|
43
|
+
# references that won't materialize before rendering time:
|
44
|
+
#
|
45
|
+
# <h1><%= @page_title %></h1>
|
46
|
+
# <%= yield %>
|
47
|
+
#
|
48
|
+
# ...and content pages that fulfill these references _at_ rendering time:
|
49
|
+
#
|
50
|
+
# <% @page_title = "Welcome" %>
|
51
|
+
# Off-world colonies offers you a chance to start a new life
|
52
|
+
#
|
53
|
+
# The result after rendering is:
|
54
|
+
#
|
55
|
+
# <h1>Welcome</h1>
|
56
|
+
# Off-world colonies offers you a chance to start a new life
|
57
|
+
#
|
58
|
+
# == Automatic layout assignment
|
59
|
+
#
|
60
|
+
# If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically
|
61
|
+
# set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named
|
62
|
+
# <tt>app/views/layouts/weblog.erb</tt> or <tt>app/views/layouts/weblog.builder</tt> exists then it will be automatically set as
|
63
|
+
# the layout for your WeblogController. You can create a layout with the name <tt>application.erb</tt> or <tt>application.builder</tt>
|
64
|
+
# 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
|
65
|
+
# no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout.
|
66
|
+
# assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.erb</tt>.
|
67
|
+
# Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set.
|
68
|
+
# Explicitly setting the layout in a parent class, though, will not override the child class's layout assignment if the child
|
69
|
+
# class has a layout with the same name.
|
70
|
+
#
|
71
|
+
# == Inheritance for layouts
|
72
|
+
#
|
73
|
+
# Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
|
74
|
+
#
|
75
|
+
# class BankController < ActionController::Base
|
76
|
+
# layout "bank_standard"
|
77
|
+
#
|
78
|
+
# class InformationController < BankController
|
79
|
+
#
|
80
|
+
# class VaultController < BankController
|
81
|
+
# layout :access_level_layout
|
82
|
+
#
|
83
|
+
# class EmployeeController < BankController
|
84
|
+
# layout nil
|
85
|
+
#
|
86
|
+
# The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
|
87
|
+
# and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
|
88
|
+
#
|
89
|
+
# == Types of layouts
|
90
|
+
#
|
91
|
+
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
|
92
|
+
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
|
93
|
+
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
|
94
|
+
#
|
95
|
+
# The method reference is the preferred approach to variable layouts and is used like this:
|
96
|
+
#
|
97
|
+
# class WeblogController < ActionController::Base
|
98
|
+
# layout :writers_and_readers
|
99
|
+
#
|
100
|
+
# def index
|
101
|
+
# # fetching posts
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# private
|
105
|
+
# def writers_and_readers
|
106
|
+
# logged_in? ? "writer_layout" : "reader_layout"
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
|
110
|
+
# is logged in or not.
|
111
|
+
#
|
112
|
+
# If you want to use an inline method, such as a proc, do something like this:
|
113
|
+
#
|
114
|
+
# class WeblogController < ActionController::Base
|
115
|
+
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
|
116
|
+
#
|
117
|
+
# Of course, the most common way of specifying a layout is still just as a plain template name:
|
118
|
+
#
|
119
|
+
# class WeblogController < ActionController::Base
|
120
|
+
# layout "weblog_standard"
|
121
|
+
#
|
122
|
+
# If no directory is specified for the template name, the template will by default be looked for in +app/views/layouts/+.
|
123
|
+
# Otherwise, it will be looked up relative to the template root.
|
124
|
+
#
|
125
|
+
# == Conditional layouts
|
126
|
+
#
|
127
|
+
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
|
128
|
+
# 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
|
129
|
+
# <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
|
130
|
+
#
|
131
|
+
# class WeblogController < ActionController::Base
|
132
|
+
# layout "weblog_standard", :except => :rss
|
133
|
+
#
|
134
|
+
# # ...
|
135
|
+
#
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
|
139
|
+
# 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
|
142
|
+
# #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
|
143
|
+
#
|
144
|
+
# == Using a different layout in the action render call
|
145
|
+
#
|
146
|
+
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
|
147
|
+
# Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
|
148
|
+
# This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
|
149
|
+
# qualified template and layout names as this example shows:
|
150
|
+
#
|
151
|
+
# class WeblogController < ActionController::Base
|
152
|
+
# def help
|
153
|
+
# render :action => "help/index", :layout => "help"
|
154
|
+
# end
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# As you can see, you pass the template as the first parameter, the status code as the second ("200" is OK), and the layout
|
158
|
+
# as the third.
|
159
|
+
#
|
160
|
+
# NOTE: The old notation for rendering the view from a layout was to expose the magic <tt>@content_for_layout</tt> instance
|
161
|
+
# variable. The preferred notation now is to use <tt>yield</tt>, as documented above.
|
162
|
+
module ClassMethods
|
163
|
+
# If a layout is specified, all rendered actions will have their result rendered
|
164
|
+
# when the layout <tt>yield</tt>s. This layout can itself depend on instance variables assigned during action
|
165
|
+
# performance and have access to them as any normal template would.
|
166
|
+
def layout(template_name, conditions = {}, auto = false)
|
167
|
+
add_layout_conditions(conditions)
|
168
|
+
write_inheritable_attribute(:layout, template_name)
|
169
|
+
write_inheritable_attribute(:auto_layout, auto)
|
170
|
+
end
|
171
|
+
|
172
|
+
def layout_conditions #:nodoc:
|
173
|
+
@layout_conditions ||= read_inheritable_attribute(:layout_conditions)
|
174
|
+
end
|
175
|
+
|
176
|
+
def default_layout(format) #:nodoc:
|
177
|
+
layout = read_inheritable_attribute(:layout)
|
178
|
+
return layout unless read_inheritable_attribute(:auto_layout)
|
179
|
+
@default_layout ||= {}
|
180
|
+
@default_layout[format] ||= default_layout_with_format(format, layout)
|
181
|
+
@default_layout[format]
|
182
|
+
end
|
183
|
+
|
184
|
+
def layout_list #:nodoc:
|
185
|
+
Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] }
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
def inherited_with_layout(child)
|
190
|
+
inherited_without_layout(child)
|
191
|
+
unless child.name.blank?
|
192
|
+
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
|
193
|
+
child.layout(layout_match, {}, true) unless child.layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty?
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_layout_conditions(conditions)
|
198
|
+
write_inheritable_hash(:layout_conditions, normalize_conditions(conditions))
|
199
|
+
end
|
200
|
+
|
201
|
+
def normalize_conditions(conditions)
|
202
|
+
conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
|
203
|
+
end
|
204
|
+
|
205
|
+
def default_layout_with_format(format, layout)
|
206
|
+
list = layout_list
|
207
|
+
if list.grep(%r{layouts/#{layout}\.#{format}(\.[a-z][0-9a-z]*)+$}).empty?
|
208
|
+
(!list.grep(%r{layouts/#{layout}\.([a-z][0-9a-z]*)+$}).empty? && format == :rb) ? layout : nil
|
209
|
+
else
|
210
|
+
layout
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
|
216
|
+
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
|
217
|
+
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
|
218
|
+
# weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
|
219
|
+
def active_layout(passed_layout = nil)
|
220
|
+
layout = passed_layout || self.class.default_layout(default_template_format)
|
221
|
+
active_layout = case layout
|
222
|
+
when String then layout
|
223
|
+
when Symbol then send(layout)
|
224
|
+
when Proc then layout.call(self)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Explicitly passed layout names with slashes are looked up relative to the template root,
|
228
|
+
# but auto-discovered layouts derived from a nested controller will contain a slash, though be relative
|
229
|
+
# to the 'layouts' directory so we have to check the file system to infer which case the layout name came from.
|
230
|
+
if active_layout
|
231
|
+
if active_layout.include?('/') && ! layout_directory?(active_layout)
|
232
|
+
active_layout
|
233
|
+
else
|
234
|
+
"layouts/#{active_layout}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
def candidate_for_layout?(options)
|
241
|
+
options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? &&
|
242
|
+
!@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action]))
|
243
|
+
end
|
244
|
+
|
245
|
+
def pick_layout(options)
|
246
|
+
if options.has_key?(:layout)
|
247
|
+
case layout = options.delete(:layout)
|
248
|
+
when FalseClass
|
249
|
+
nil
|
250
|
+
when NilClass, TrueClass
|
251
|
+
active_layout if action_has_layout? && !@template.__send__(:_exempt_from_layout?, default_template_name)
|
252
|
+
else
|
253
|
+
active_layout(layout)
|
254
|
+
end
|
255
|
+
else
|
256
|
+
active_layout if action_has_layout? && candidate_for_layout?(options)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def action_has_layout?
|
261
|
+
if conditions = self.class.layout_conditions
|
262
|
+
case
|
263
|
+
when only = conditions[:only]
|
264
|
+
only.include?(action_name)
|
265
|
+
when except = conditions[:except]
|
266
|
+
!except.include?(action_name)
|
267
|
+
else
|
268
|
+
true
|
269
|
+
end
|
270
|
+
else
|
271
|
+
true
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def layout_directory?(layout_name)
|
276
|
+
@template.template_exists?("#{File.join('layouts', layout_name)}.#{@template.template_format}")
|
277
|
+
rescue Reactive::View::MissingTemplate
|
278
|
+
false
|
279
|
+
end
|
280
|
+
|
281
|
+
def default_template_format
|
282
|
+
response.template.template_format
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|