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,38 @@
|
|
1
|
+
module Reactive::Mvc::View
|
2
|
+
module Helpers
|
3
|
+
def inject_output_helpers
|
4
|
+
<<-EOS
|
5
|
+
helpers_module = HelperModule.new #{output_helper_names.inspect}, <<-EOC
|
6
|
+
#{read_output_helpers.gsub('#', '\#')}
|
7
|
+
EOC
|
8
|
+
extend(*helpers_module.helpers)
|
9
|
+
EOS
|
10
|
+
end
|
11
|
+
|
12
|
+
def output_helper_names
|
13
|
+
parts = controller_name.split('/')
|
14
|
+
(["application_helper.#{request.format}.rb"] + Enumerable::Enumerator.new(parts, :each_index).collect{|i| "#{parts[0..i].join('/')}_helper.#{request.format}.rb"}).uniq.select{|path| File.file?(Reactive.path_for(:helper, path))}
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_output_helpers(names = output_helper_names)
|
18
|
+
names.inject("") {|code, path| code << File.read(Reactive.path_for(:helper, path))}
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
=begin
|
25
|
+
def include_application_helpers(request)
|
26
|
+
include_helper('application_helper')
|
27
|
+
parts = controller_name.split('/')
|
28
|
+
Enumerable::Enumerator.new(parts, :each_index).collect{|i| parts[0..i].join('/')}.each do |path|
|
29
|
+
include_helper("#{path}_helper")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def include_helper(name)
|
34
|
+
@helpers.include(name.classify.constantize)
|
35
|
+
rescue NameError, LoadError
|
36
|
+
# Helper files are not mandatory
|
37
|
+
end
|
38
|
+
=end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module Reactive::Mvc::View
|
2
|
+
# There's also a convenience method for rendering sub templates within the current controller that depends on a
|
3
|
+
# single object (we call this kind of sub templates for partials). It relies on the fact that partials should
|
4
|
+
# follow the naming convention of being prefixed with an underscore -- as to separate them from regular
|
5
|
+
# templates that could be rendered on their own.
|
6
|
+
#
|
7
|
+
# In a template for Advertiser#account:
|
8
|
+
#
|
9
|
+
# <%= render :partial => "account" %>
|
10
|
+
#
|
11
|
+
# This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable
|
12
|
+
# +account+ to the template for display.
|
13
|
+
#
|
14
|
+
# In another template for Advertiser#buy, we could have:
|
15
|
+
#
|
16
|
+
# <%= render :partial => "account", :locals => { :account => @buyer } %>
|
17
|
+
#
|
18
|
+
# <% for ad in @advertisements %>
|
19
|
+
# <%= render :partial => "ad", :locals => { :ad => ad } %>
|
20
|
+
# <% end %>
|
21
|
+
#
|
22
|
+
# This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then
|
23
|
+
# render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display.
|
24
|
+
#
|
25
|
+
# == Rendering a collection of partials
|
26
|
+
#
|
27
|
+
# The example of partial use describes a familiar pattern where a template needs to iterate over an array and
|
28
|
+
# render a sub template for each of the elements. This pattern has been implemented as a single method that
|
29
|
+
# accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
|
30
|
+
# example in "Using partials" can be rewritten with a single line:
|
31
|
+
#
|
32
|
+
# <%= render :partial => "ad", :collection => @advertisements %>
|
33
|
+
#
|
34
|
+
# This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An
|
35
|
+
# iteration counter will automatically be made available to the template with a name of the form
|
36
|
+
# +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
|
37
|
+
#
|
38
|
+
# NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
|
39
|
+
# just keep domain objects, like Active Records, in there.
|
40
|
+
#
|
41
|
+
# == Rendering shared partials
|
42
|
+
#
|
43
|
+
# Two controllers can share a set of partials and render them like this:
|
44
|
+
#
|
45
|
+
# <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %>
|
46
|
+
#
|
47
|
+
# This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from.
|
48
|
+
#
|
49
|
+
# == Rendering partials with layouts
|
50
|
+
#
|
51
|
+
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
|
52
|
+
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
|
53
|
+
# of users:
|
54
|
+
#
|
55
|
+
# <%# app/views/users/index.html.erb &>
|
56
|
+
# Here's the administrator:
|
57
|
+
# <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %>
|
58
|
+
#
|
59
|
+
# Here's the editor:
|
60
|
+
# <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %>
|
61
|
+
#
|
62
|
+
# <%# app/views/users/_user.html.erb &>
|
63
|
+
# Name: <%= user.name %>
|
64
|
+
#
|
65
|
+
# <%# app/views/users/_administrator.html.erb &>
|
66
|
+
# <div id="administrator">
|
67
|
+
# Budget: $<%= user.budget %>
|
68
|
+
# <%= yield %>
|
69
|
+
# </div>
|
70
|
+
#
|
71
|
+
# <%# app/views/users/_editor.html.erb &>
|
72
|
+
# <div id="editor">
|
73
|
+
# Deadline: <%= user.deadline %>
|
74
|
+
# <%= yield %>
|
75
|
+
# </div>
|
76
|
+
#
|
77
|
+
# ...this will return:
|
78
|
+
#
|
79
|
+
# Here's the administrator:
|
80
|
+
# <div id="administrator">
|
81
|
+
# Budget: $<%= user.budget %>
|
82
|
+
# Name: <%= user.name %>
|
83
|
+
# </div>
|
84
|
+
#
|
85
|
+
# Here's the editor:
|
86
|
+
# <div id="editor">
|
87
|
+
# Deadline: <%= user.deadline %>
|
88
|
+
# Name: <%= user.name %>
|
89
|
+
# </div>
|
90
|
+
#
|
91
|
+
# You can also apply a layout to a block within any template:
|
92
|
+
#
|
93
|
+
# <%# app/views/users/_chief.html.erb &>
|
94
|
+
# <% render(:layout => "administrator", :locals => { :user => chief }) do %>
|
95
|
+
# Title: <%= chief.title %>
|
96
|
+
# <% end %>
|
97
|
+
#
|
98
|
+
# ...this will return:
|
99
|
+
#
|
100
|
+
# <div id="administrator">
|
101
|
+
# Budget: $<%= user.budget %>
|
102
|
+
# Title: <%= chief.name %>
|
103
|
+
# </div>
|
104
|
+
#
|
105
|
+
# As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
|
106
|
+
#
|
107
|
+
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
|
108
|
+
# an array to layout and treat it as an enumerable.
|
109
|
+
#
|
110
|
+
# <%# app/views/users/_user.html.erb &>
|
111
|
+
# <div class="user">
|
112
|
+
# Budget: $<%= user.budget %>
|
113
|
+
# <%= yield user %>
|
114
|
+
# </div>
|
115
|
+
#
|
116
|
+
# <%# app/views/users/index.html.erb &>
|
117
|
+
# <% render :layout => @users do |user| %>
|
118
|
+
# Title: <%= user.title %>
|
119
|
+
# <% end %>
|
120
|
+
#
|
121
|
+
# This will render the layout for each user and yield to the block, passing the user, each time.
|
122
|
+
#
|
123
|
+
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
|
124
|
+
#
|
125
|
+
# <%# app/views/users/_user.html.erb &>
|
126
|
+
# <div class="user">
|
127
|
+
# <%= yield user, :header %>
|
128
|
+
# Budget: $<%= user.budget %>
|
129
|
+
# <%= yield user, :footer %>
|
130
|
+
# </div>
|
131
|
+
#
|
132
|
+
# <%# app/views/users/index.html.erb &>
|
133
|
+
# <% render :layout => @users do |user, section| %>
|
134
|
+
# <%- case section when :header -%>
|
135
|
+
# Title: <%= user.title %>
|
136
|
+
# <%- when :footer -%>
|
137
|
+
# Deadline: <%= user.deadline %>
|
138
|
+
# <%- end -%>
|
139
|
+
# <% end %>
|
140
|
+
module Partials
|
141
|
+
extend ActiveSupport::Memoizable
|
142
|
+
|
143
|
+
private
|
144
|
+
def render_partial(options = {}) #:nodoc:
|
145
|
+
local_assigns = options[:locals] || {}
|
146
|
+
|
147
|
+
case partial_path = options[:partial]
|
148
|
+
when String, Symbol, NilClass
|
149
|
+
if options.has_key?(:collection)
|
150
|
+
render_partial_collection(options)
|
151
|
+
else
|
152
|
+
_pick_partial_template(partial_path).render_partial(self, options[:object], local_assigns)
|
153
|
+
end
|
154
|
+
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope
|
155
|
+
render_partial_collection(options.except(:partial).merge(:collection => partial_path))
|
156
|
+
else
|
157
|
+
raise Error, "RecordIdentifier not available"
|
158
|
+
object = partial_path
|
159
|
+
render_partial(
|
160
|
+
:partial => ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path),
|
161
|
+
:object => object,
|
162
|
+
:locals => local_assigns
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def render_partial_collection(options = {}) #:nodoc:
|
168
|
+
return nil if options[:collection].blank?
|
169
|
+
|
170
|
+
partial = options[:partial]
|
171
|
+
spacer = options[:spacer_template] ? render(:partial => options[:spacer_template]) : ''
|
172
|
+
local_assigns = options[:locals] ? options[:locals].clone : {}
|
173
|
+
as = options[:as]
|
174
|
+
|
175
|
+
index = 0
|
176
|
+
options[:collection].map do |object|
|
177
|
+
_partial_path ||= partial #||
|
178
|
+
# ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path)
|
179
|
+
template = _pick_partial_template(_partial_path)
|
180
|
+
local_assigns[template.counter_name] = index
|
181
|
+
result = template.render_partial(self, object, local_assigns.dup, as)
|
182
|
+
index += 1
|
183
|
+
result
|
184
|
+
end.join(spacer)
|
185
|
+
end
|
186
|
+
|
187
|
+
def _pick_partial_template(partial_path) #:nodoc:
|
188
|
+
if partial_path.include?('/')
|
189
|
+
path = File.join(File.dirname(partial_path), "_#{File.basename(partial_path)}")
|
190
|
+
elsif controller
|
191
|
+
path = "#{controller.class.controller_path}/_#{partial_path}"
|
192
|
+
else
|
193
|
+
path = "_#{partial_path}"
|
194
|
+
end
|
195
|
+
|
196
|
+
_pick_template(path)
|
197
|
+
end
|
198
|
+
memoize :_pick_partial_template
|
199
|
+
|
200
|
+
def partial_template_exists?(partial_path)
|
201
|
+
_pick_partial_template(partial_path) ? true : false
|
202
|
+
rescue MissingTemplate
|
203
|
+
false
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Reactive::Mvc::View #:nodoc:
|
2
|
+
class PathSet < Array #:nodoc:
|
3
|
+
def self.type_cast(obj)
|
4
|
+
if obj.is_a?(String)
|
5
|
+
if Base.warn_cache_misses && defined?(Rails) && Rails.initialized?
|
6
|
+
Base.logger.debug "[PERFORMANCE] Processing view path during a " +
|
7
|
+
"request. This an expense disk operation that should be done at " +
|
8
|
+
"boot. You can manually process this view path with " +
|
9
|
+
"ActionView::Base.process_view_paths(#{obj.inspect}) and set it " +
|
10
|
+
"as your view path"
|
11
|
+
end
|
12
|
+
Path.new(obj)
|
13
|
+
else
|
14
|
+
obj
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super(*args).map! { |obj| self.class.type_cast(obj) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(obj)
|
23
|
+
super(self.class.type_cast(obj))
|
24
|
+
end
|
25
|
+
|
26
|
+
def concat(array)
|
27
|
+
super(array.map! { |obj| self.class.type_cast(obj) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def insert(index, obj)
|
31
|
+
super(index, self.class.type_cast(obj))
|
32
|
+
end
|
33
|
+
|
34
|
+
def push(*objs)
|
35
|
+
super(*objs.map { |obj| self.class.type_cast(obj) })
|
36
|
+
end
|
37
|
+
|
38
|
+
def unshift(*objs)
|
39
|
+
super(*objs.map { |obj| self.class.type_cast(obj) })
|
40
|
+
end
|
41
|
+
|
42
|
+
class Path #:nodoc:
|
43
|
+
def self.eager_load_templates!
|
44
|
+
@eager_load_templates = true
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.eager_load_templates?
|
48
|
+
@eager_load_templates || false
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :path, :paths
|
52
|
+
delegate :to_s, :to_str, :hash, :inspect, :to => :path
|
53
|
+
|
54
|
+
def initialize(path, load = true)
|
55
|
+
raise ArgumentError, "path already is a Path class" if path.is_a?(Path)
|
56
|
+
@path = path.freeze
|
57
|
+
reload! if load
|
58
|
+
end
|
59
|
+
|
60
|
+
def ==(path)
|
61
|
+
to_str == path.to_str
|
62
|
+
end
|
63
|
+
|
64
|
+
def eql?(path)
|
65
|
+
to_str == path.to_str
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](path)
|
69
|
+
raise "Unloaded view path! #{@path}" unless @loaded
|
70
|
+
@paths[path]
|
71
|
+
end
|
72
|
+
|
73
|
+
def loaded?
|
74
|
+
@loaded ? true : false
|
75
|
+
end
|
76
|
+
|
77
|
+
def load
|
78
|
+
reload! unless loaded?
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Rebuild load path directory cache
|
83
|
+
def reload!
|
84
|
+
@paths = {}
|
85
|
+
|
86
|
+
templates_in_path do |template|
|
87
|
+
# Eager load memoized methods and freeze cached template
|
88
|
+
template.freeze if self.class.eager_load_templates?
|
89
|
+
|
90
|
+
@paths[template.path] = template
|
91
|
+
@paths[template.path_without_extension] ||= template
|
92
|
+
end
|
93
|
+
|
94
|
+
@paths.freeze
|
95
|
+
@loaded = true
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def templates_in_path
|
100
|
+
(Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file|
|
101
|
+
unless File.directory?(file)
|
102
|
+
yield Template.new(file.split("#{self}/").last, self)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def load
|
109
|
+
each { |path| path.load }
|
110
|
+
end
|
111
|
+
|
112
|
+
def reload!
|
113
|
+
each { |path| path.reload! }
|
114
|
+
end
|
115
|
+
|
116
|
+
def [](template_path)
|
117
|
+
each do |path|
|
118
|
+
if template = path[template_path]
|
119
|
+
return template
|
120
|
+
end
|
121
|
+
end
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Reactive::Mvc
|
2
|
+
module View
|
3
|
+
# NOTE: The template that this mixin is being included into is frozen
|
4
|
+
# so you cannot set or modify any instance variables
|
5
|
+
module Renderable #:nodoc:
|
6
|
+
extend ActiveSupport::Memoizable
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
@@mutex = Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def filename
|
13
|
+
'compiled-template'
|
14
|
+
end
|
15
|
+
|
16
|
+
def handler
|
17
|
+
Template.handler_class_for_extension(extension)
|
18
|
+
end
|
19
|
+
memoize :handler
|
20
|
+
|
21
|
+
def compiled_source
|
22
|
+
handler.call(self)
|
23
|
+
end
|
24
|
+
memoize :compiled_source
|
25
|
+
|
26
|
+
def render(view, local_assigns = {})
|
27
|
+
compile(local_assigns)
|
28
|
+
|
29
|
+
stack = view.instance_variable_get(:@_render_stack)
|
30
|
+
stack.push(self)
|
31
|
+
|
32
|
+
view.send(:_evaluate_assigns_and_ivars)
|
33
|
+
|
34
|
+
result = view.send(method_name(local_assigns), local_assigns) do |*names|
|
35
|
+
ivar = :@_proc_for_layout
|
36
|
+
if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
|
37
|
+
view.capture(*names, &proc)
|
38
|
+
elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
|
39
|
+
view.instance_variable_get(ivar)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
stack.pop
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_name(local_assigns)
|
48
|
+
if local_assigns && local_assigns.any?
|
49
|
+
local_assigns_keys = "locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
|
50
|
+
end
|
51
|
+
['_run', extension, method_segment, local_assigns_keys].compact.join('_').to_sym
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
# Compile and evaluate the template's code (if necessary)
|
56
|
+
def compile(local_assigns)
|
57
|
+
render_symbol = method_name(local_assigns)
|
58
|
+
|
59
|
+
@@mutex.synchronize do
|
60
|
+
if recompile?(render_symbol)
|
61
|
+
compile!(render_symbol, local_assigns)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def compile!(render_symbol, local_assigns)
|
67
|
+
locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
|
68
|
+
|
69
|
+
source = <<-end_src
|
70
|
+
def #{render_symbol}(local_assigns)
|
71
|
+
old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
|
72
|
+
ensure
|
73
|
+
self.output_buffer = old_output_buffer
|
74
|
+
end
|
75
|
+
end_src
|
76
|
+
|
77
|
+
begin
|
78
|
+
Reactive::Mvc::View::Base::CompiledTemplates.module_eval(source, filename, 0)
|
79
|
+
rescue Exception => e # errors from template code
|
80
|
+
if logger = Base.logger
|
81
|
+
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
|
82
|
+
logger.debug "Function body: #{source}"
|
83
|
+
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
|
84
|
+
end
|
85
|
+
|
86
|
+
raise TemplateError.new(relative_path, source, e)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Method to check whether template compilation is necessary.
|
91
|
+
# The template will be compiled if the file has not been compiled yet, or
|
92
|
+
# if local_assigns has a new key, which isn't supported by the compiled code yet.
|
93
|
+
def recompile?(symbol)
|
94
|
+
!(Reactive::Mvc::View::PathSet::Path.eager_load_templates? && Base::CompiledTemplates.method_defined?(symbol))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|