view_models 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +26 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +242 -0
- data/Rakefile +47 -0
- data/VERSION.yml +4 -0
- data/generators/view_models/USAGE +6 -0
- data/generators/view_models/templates/README +1 -0
- data/generators/view_models/templates/spec/view_model_spec.rb +7 -0
- data/generators/view_models/templates/view_models/view_model.rb +5 -0
- data/generators/view_models/templates/views/_empty.html.haml +0 -0
- data/generators/view_models/templates/views/view_models/collection/_collection.html.erb +1 -0
- data/generators/view_models/templates/views/view_models/collection/_collection.html.haml +8 -0
- data/generators/view_models/templates/views/view_models/collection/_collection.text.erb +6 -0
- data/generators/view_models/templates/views/view_models/collection/_list.html.erb +1 -0
- data/generators/view_models/templates/views/view_models/collection/_list.html.haml +7 -0
- data/generators/view_models/templates/views/view_models/collection/_list.text.erb +6 -0
- data/generators/view_models/templates/views/view_models/collection/_pagination.html.haml +12 -0
- data/generators/view_models/templates/views/view_models/collection/_pagination.text.erb +3 -0
- data/generators/view_models/templates/views/view_models/collection/_table.html.haml +5 -0
- data/generators/view_models/templates/views/view_models/collection/_table.text.erb +10 -0
- data/generators/view_models/view_models_generator.rb +47 -0
- data/lib/extensions/active_record.rb +19 -0
- data/lib/extensions/model_reader.rb +115 -0
- data/lib/extensions/view.rb +24 -0
- data/lib/helpers/collection.rb +124 -0
- data/lib/helpers/rails.rb +59 -0
- data/lib/helpers/view.rb +22 -0
- data/lib/view_models/base.rb +268 -0
- data/lib/view_models/controller_extractor.rb +24 -0
- data/lib/view_models/path_store.rb +61 -0
- data/lib/view_models/render_options.rb +109 -0
- data/lib/view_models/view.rb +26 -0
- data/lib/view_models.rb +3 -0
- data/rails/init.rb +18 -0
- data/spec/integration/integration_spec.rb +269 -0
- data/spec/integration/models/sub_subclass.rb +14 -0
- data/spec/integration/models/subclass.rb +3 -0
- data/spec/integration/view_models/project.rb +14 -0
- data/spec/integration/view_models/sub_subclass.rb +42 -0
- data/spec/integration/view_models/subclass.rb +1 -0
- data/spec/integration/views/view_models/collection/_collection.html.erb +1 -0
- data/spec/integration/views/view_models/collection/_collection.text.erb +6 -0
- data/spec/integration/views/view_models/collection/_list.html.erb +1 -0
- data/spec/integration/views/view_models/collection/_list.text.erb +6 -0
- data/spec/integration/views/view_models/sub_subclass/_capture_in_template.erb +2 -0
- data/spec/integration/views/view_models/sub_subclass/_capture_in_view_model.erb +3 -0
- data/spec/integration/views/view_models/sub_subclass/_collection_example.html.erb +3 -0
- data/spec/integration/views/view_models/sub_subclass/_collection_example.text.erb +3 -0
- data/spec/integration/views/view_models/sub_subclass/_collection_item.html.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_collection_item.text.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_exists.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_exists.html.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_exists.text.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_exists_in_both.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_inner.also_explicit.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_inner.nesting.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_list_example.html.erb +3 -0
- data/spec/integration/views/view_models/sub_subclass/_list_example.text.erb +3 -0
- data/spec/integration/views/view_models/sub_subclass/_list_item.html.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_list_item.text.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_outer.explicit.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_outer.nesting.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/_part_that_is_dependent_on_the_view_model.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/show.html.erb +1 -0
- data/spec/integration/views/view_models/sub_subclass/show.text.erb +1 -0
- data/spec/integration/views/view_models/subclass/_exists_in_both.erb +1 -0
- data/spec/integration/views/view_models/subclass/_no_sub_subclass.erb +1 -0
- data/spec/integration/views/view_models/subclass/_not_found_in_sub_subclass.erb +1 -0
- data/spec/lib/extensions/active_record_spec.rb +31 -0
- data/spec/lib/extensions/model_reader_spec.rb +93 -0
- data/spec/lib/helpers/collection_spec.rb +196 -0
- data/spec/lib/helpers/rails_spec.rb +88 -0
- data/spec/lib/helpers/view_spec.rb +20 -0
- data/spec/lib/view_models/base_spec.rb +102 -0
- data/spec/lib/view_models/view_spec.rb +9 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/spec_helper_extensions.rb +13 -0
- metadata +156 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
module ViewModels
|
2
|
+
module Helpers
|
3
|
+
module Rails
|
4
|
+
|
5
|
+
# Construct a view_model for a collection.
|
6
|
+
#
|
7
|
+
def collection_view_model_for array_or_pagination, context = self
|
8
|
+
Collection.new array_or_pagination, context
|
9
|
+
end
|
10
|
+
|
11
|
+
# The Collection view_model helper has the purpose of presenting presentable collections.
|
12
|
+
# * Render as list
|
13
|
+
# * Render as table
|
14
|
+
# * Render as collection
|
15
|
+
# * Render a pagination
|
16
|
+
#
|
17
|
+
class Collection
|
18
|
+
|
19
|
+
#
|
20
|
+
#
|
21
|
+
methods_to_delegate = [Enumerable.instance_methods.map(&:to_sym),
|
22
|
+
:length, :size, :empty?, :each, :exit,
|
23
|
+
{ :to => :@collection }].flatten
|
24
|
+
self.delegate *methods_to_delegate
|
25
|
+
def select *args, &block # active_support fail?
|
26
|
+
@collection.select *args, &block
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize collection, context
|
30
|
+
@collection, @context = collection, context
|
31
|
+
end
|
32
|
+
|
33
|
+
# Renders a list (in the broadest sense of the word).
|
34
|
+
#
|
35
|
+
# Options:
|
36
|
+
# collection => collection to iterate over
|
37
|
+
# context => context to render in
|
38
|
+
# template_name => template to render for each model element
|
39
|
+
# separator => separator between each element
|
40
|
+
# By default, uses:
|
41
|
+
# * The collection of the collection view_model to iterate over.
|
42
|
+
# * The original context given to the collection view_model to render in.
|
43
|
+
# * Uses :list_item as the default element template.
|
44
|
+
# * Uses a nil separator in html.
|
45
|
+
#
|
46
|
+
def list options = {}
|
47
|
+
default_options = { :collection => @collection, :template_name => :list_item, :separator => nil }
|
48
|
+
|
49
|
+
render_partial :list, default_options.merge(options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Renders a collection.
|
53
|
+
#
|
54
|
+
# Note: The only difference between a list and a collection is the enclosing
|
55
|
+
# list type. While list uses ol, the collection uses ul.
|
56
|
+
#
|
57
|
+
# Options:
|
58
|
+
# collection => collection to iterate over
|
59
|
+
# context => context to render in
|
60
|
+
# template_name => template to render for each model element
|
61
|
+
# separator => separator between each element
|
62
|
+
# By default, uses:
|
63
|
+
# * The collection of the collection view_model to iterate over.
|
64
|
+
# * Uses :collection_item as the default element template.
|
65
|
+
# * Uses a nil separator.
|
66
|
+
#
|
67
|
+
def collection options = {}
|
68
|
+
default_options = { :collection => @collection, :template_name => :collection_item, :separator => nil }
|
69
|
+
|
70
|
+
render_partial :collection, default_options.merge(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Renders a table.
|
74
|
+
#
|
75
|
+
# Note: Each item represents a table row.
|
76
|
+
#
|
77
|
+
# Options:
|
78
|
+
# collection => collection to iterate over
|
79
|
+
# context => context to render in
|
80
|
+
# template_name => template to render for each model element
|
81
|
+
# separator => separator between each element
|
82
|
+
# By default, uses:
|
83
|
+
# * The collection of the collection view_model to iterate over.
|
84
|
+
# * Uses :table_row as the default element template.
|
85
|
+
# * Uses a nil separator.
|
86
|
+
#
|
87
|
+
def table options = {}
|
88
|
+
default_options = { :collection => @collection, :template_name => :table_row, :separator => nil }
|
89
|
+
|
90
|
+
render_partial :table, default_options.merge(options)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Renders a pagination.
|
94
|
+
#
|
95
|
+
# Options:
|
96
|
+
# collection => collection to iterate over
|
97
|
+
# context => context to render in
|
98
|
+
# separator => separator between pages
|
99
|
+
# By default, uses:
|
100
|
+
# * The collection of the collection view_model to iterate over.
|
101
|
+
# * Uses | as separator.
|
102
|
+
#
|
103
|
+
def pagination options = {}
|
104
|
+
default_options = { :collection => @collection, :separator => '|' }
|
105
|
+
|
106
|
+
render_partial :pagination, default_options.merge(options)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Helper method that renders a partial in the context of the context instance.
|
112
|
+
#
|
113
|
+
# Example:
|
114
|
+
# If the collection view_model helper has been instantiated in the context
|
115
|
+
# of a controller, render will be called in the controller.
|
116
|
+
#
|
117
|
+
def render_partial name, locals
|
118
|
+
@context.instance_eval { render :partial => "view_models/collection/#{name}", :locals => locals }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ViewModels
|
2
|
+
module Helpers
|
3
|
+
module Rails
|
4
|
+
|
5
|
+
mattr_accessor :specific_view_model_mapping
|
6
|
+
self.specific_view_model_mapping = {}
|
7
|
+
|
8
|
+
# Create a new view_model instance for the given model instance
|
9
|
+
# with the given arguments.
|
10
|
+
#
|
11
|
+
# Note: Will emit an ArgumentError if the view model class doesn't support 2 arguments.
|
12
|
+
#
|
13
|
+
def view_model_for model, context = self
|
14
|
+
view_model_class_for(model).new model, context
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get the view_model class for the given model instance.
|
18
|
+
#
|
19
|
+
# Note: ViewModels are usually of class ViewModels::<ModelClassName>.
|
20
|
+
# (As returned by default_view_model_class_for)
|
21
|
+
# Override specific_mapping if you'd like to install your own.
|
22
|
+
#
|
23
|
+
# OR: Override default_view_model_class_for(model) if
|
24
|
+
# you'd like to change the default.
|
25
|
+
#
|
26
|
+
def view_model_class_for model
|
27
|
+
specific_view_model_class_for(model) || default_view_model_class_for(model)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the default view_model class for the given model instance.
|
31
|
+
#
|
32
|
+
# Default class name is:
|
33
|
+
# ViewModels::<ModelClassName>
|
34
|
+
#
|
35
|
+
# Override this method if you'd like to change the _default_
|
36
|
+
# model-to-view_model class mapping.
|
37
|
+
#
|
38
|
+
# Note: Will emit a NameError if a corresponding ViewModels constant cannot be loaded.
|
39
|
+
#
|
40
|
+
mattr_accessor :default_prefix
|
41
|
+
self.default_prefix = 'ViewModels::'
|
42
|
+
def default_view_model_class_for model
|
43
|
+
(default_prefix + model.class.name).constantize
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a specific view_model class for the given model instance.
|
47
|
+
#
|
48
|
+
# Override this method, if you want to return a specific
|
49
|
+
# view model class for the given model.
|
50
|
+
#
|
51
|
+
# Note: Will emit a NameError if a corresponding ViewModels constant cannot be loaded.
|
52
|
+
#
|
53
|
+
def specific_view_model_class_for model
|
54
|
+
specific_view_model_mapping[model.class]
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/helpers/view.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module ViewModels
|
2
|
+
module Helpers
|
3
|
+
# Module for conveniently including common view_helpers into a view_model
|
4
|
+
#
|
5
|
+
module View
|
6
|
+
|
7
|
+
# Include hook.
|
8
|
+
#
|
9
|
+
def self.included view_model
|
10
|
+
view_model.send :include, *all_view_helpers
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.all_view_helpers
|
14
|
+
[
|
15
|
+
ActionView::Helpers,
|
16
|
+
ERB::Util
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# Base Module for ViewModels.
|
2
|
+
#
|
3
|
+
module ViewModels
|
4
|
+
|
5
|
+
# Gets raised when render_as, render_the, or render_template cannot
|
6
|
+
# find the named template, not even in the hierarchy.
|
7
|
+
#
|
8
|
+
class MissingTemplateError < StandardError; end
|
9
|
+
|
10
|
+
# Base class from which all view_models inherit.
|
11
|
+
#
|
12
|
+
class Base
|
13
|
+
|
14
|
+
# Model and Controller are accessible from outside.
|
15
|
+
#
|
16
|
+
# TODO but they actually shouldn't be. Try to migrate into protected area.
|
17
|
+
#
|
18
|
+
attr_reader :model, :controller
|
19
|
+
|
20
|
+
# Make helper and helper_method available
|
21
|
+
#
|
22
|
+
include ActionController::Helpers
|
23
|
+
|
24
|
+
# Create a view_model. To create a view_model, you need to have a model (to present) and a context.
|
25
|
+
# The context is usually a view or a controller, but doesn't need to be.
|
26
|
+
#
|
27
|
+
def initialize model, context
|
28
|
+
@model = model
|
29
|
+
@controller = ControllerExtractor.new(context).extract
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
# Installs a path store, a specific store for
|
35
|
+
# template inheritance, to remember specific
|
36
|
+
# [path, name, format] tuples, pointing to a template path,
|
37
|
+
# so the view models don't have to traverse the inheritance chain always.
|
38
|
+
#
|
39
|
+
attr_accessor :path_store
|
40
|
+
def inherited subclass
|
41
|
+
ViewModels::PathStore.install_in subclass
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
# Installs the model_reader Method for filtered
|
46
|
+
# model method delegation.
|
47
|
+
#
|
48
|
+
include Extensions::ModelReader
|
49
|
+
|
50
|
+
# Delegates method calls to the controller.
|
51
|
+
#
|
52
|
+
# Examples:
|
53
|
+
# controller_method :current_user
|
54
|
+
# controller_method :current_user, :current_profile # multiple methods to be delegated
|
55
|
+
#
|
56
|
+
# In the view_model:
|
57
|
+
# self.current_user
|
58
|
+
# will call
|
59
|
+
# controller.current_user
|
60
|
+
#
|
61
|
+
def controller_method *methods
|
62
|
+
delegate *methods << { :to => :controller }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Wrapper for add_template_helper in ActionController::Helpers, also
|
66
|
+
# includes given helper in the view_model
|
67
|
+
#
|
68
|
+
# TODO extract into module
|
69
|
+
#
|
70
|
+
alias old_add_template_helper add_template_helper
|
71
|
+
def add_template_helper helper_module
|
72
|
+
include helper_module
|
73
|
+
old_add_template_helper helper_module
|
74
|
+
end
|
75
|
+
|
76
|
+
# Sets the view format and tries to render the given options.
|
77
|
+
#
|
78
|
+
# Note: Also caches [path, name, format] => template path.
|
79
|
+
#
|
80
|
+
def render view, options
|
81
|
+
options.format! view
|
82
|
+
path_store.cached options do
|
83
|
+
options.file = template_path view, options
|
84
|
+
view.render_with options
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
# Returns the next view model class in the render hierarchy.
|
91
|
+
#
|
92
|
+
# Note: Just returns the superclass.
|
93
|
+
#
|
94
|
+
# TODO Think about raising the MissingTemplateError here.
|
95
|
+
#
|
96
|
+
def next
|
97
|
+
superclass
|
98
|
+
end
|
99
|
+
|
100
|
+
# Just raises a fitting template error.
|
101
|
+
#
|
102
|
+
def raise_template_error_with message
|
103
|
+
raise MissingTemplateError.new "No template #{message} found."
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check if the view lookup inheritance chain has ended.
|
107
|
+
#
|
108
|
+
# Raises a MissingTemplateError if yes.
|
109
|
+
#
|
110
|
+
def inheritance_chain_ends?
|
111
|
+
self == ViewModels::Base
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a template path for the view with the given options.
|
115
|
+
#
|
116
|
+
# If no template is found, traverses up the inheritance chain.
|
117
|
+
#
|
118
|
+
# Raises a MissingTemplateError if none is found during
|
119
|
+
# inheritance chain traversal.
|
120
|
+
#
|
121
|
+
def template_path view, options
|
122
|
+
raise_template_error_with options.error_message if inheritance_chain_ends?
|
123
|
+
|
124
|
+
template_path_from(view, options) || self.next.template_path(view, options)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Accesses the view to find a suitable template path.
|
128
|
+
#
|
129
|
+
def template_path_from view, options
|
130
|
+
template = view.find_template tentative_template_path(options)
|
131
|
+
|
132
|
+
template && template.path
|
133
|
+
end
|
134
|
+
|
135
|
+
# Return as render path either a stored path or a newly generated one.
|
136
|
+
#
|
137
|
+
# If nothing or nil is passed, the store is ignored.
|
138
|
+
#
|
139
|
+
def tentative_template_path options
|
140
|
+
path_store[options.path_key] || generate_template_path_from(options)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns the root of this view_models views with the template name appended.
|
144
|
+
# e.g. 'view_models/some/specific/path/to/template'
|
145
|
+
#
|
146
|
+
def generate_template_path_from options
|
147
|
+
File.join generate_path_from(options), options.name
|
148
|
+
end
|
149
|
+
|
150
|
+
# If the path is explicitly defined, return it, otherwise
|
151
|
+
# generate a view model path from the class name.
|
152
|
+
#
|
153
|
+
def generate_path_from options
|
154
|
+
options.path || view_model_path
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns the path from the view_model_view_paths to the actual templates.
|
158
|
+
# e.g. "view_models/models/book"
|
159
|
+
#
|
160
|
+
# If the class is named
|
161
|
+
# ViewModels::Models::Book
|
162
|
+
# this method will yield
|
163
|
+
# view_models/models/book
|
164
|
+
#
|
165
|
+
# Note: Remembers the result since it is dependent on the Class name only.
|
166
|
+
#
|
167
|
+
def view_model_path
|
168
|
+
@view_model_path || @view_model_path = self.name.underscore
|
169
|
+
end
|
170
|
+
|
171
|
+
end # class << self
|
172
|
+
|
173
|
+
# Delegate controller methods.
|
174
|
+
#
|
175
|
+
controller_method :logger, :form_authenticity_token, :protect_against_forgery?, :request_forgery_protection_token
|
176
|
+
|
177
|
+
# Make all the dynamically generated routes (restful routes etc.)
|
178
|
+
# available in the view_model
|
179
|
+
#
|
180
|
+
ActionController::Routing::Routes.install_helpers self
|
181
|
+
|
182
|
+
# Renders the given partial in the view_model's view root in the format given.
|
183
|
+
#
|
184
|
+
# Example:
|
185
|
+
# app/views/view_models/this/view_model/_partial.haml
|
186
|
+
# app/views/view_models/this/view_model/_partial.text.erb
|
187
|
+
#
|
188
|
+
# The following options are supported:
|
189
|
+
# * :format - Calling view_model.render_as('partial') will render the haml
|
190
|
+
# partial, calling view_model.render_as('partial', :format => :text) will render
|
191
|
+
# the text erb.
|
192
|
+
# * All other options are passed on to the render call. I.e. if you want to specify locals you can call
|
193
|
+
# view_model.render_as(:partial, :locals => { :name => :value })
|
194
|
+
# * If no format is given, it will render the default format, which is (currently) html.
|
195
|
+
#
|
196
|
+
def render_as name, options = {}
|
197
|
+
render RenderOptions::Partial.new(name, options)
|
198
|
+
end
|
199
|
+
# render_the is used for small parts.
|
200
|
+
#
|
201
|
+
# Example:
|
202
|
+
# # If the view_model is called window, the following
|
203
|
+
# # is more legible than window.render_as :menubar
|
204
|
+
# * window.render_the :menubar
|
205
|
+
#
|
206
|
+
alias render_the render_as
|
207
|
+
|
208
|
+
# Renders the given template in the view_model's view root in the format given.
|
209
|
+
#
|
210
|
+
# Example:
|
211
|
+
# app/views/view_models/this/view_model/template.haml
|
212
|
+
# app/views/view_models/this/view_model/template name.text.erb
|
213
|
+
#
|
214
|
+
# The following options are supported:
|
215
|
+
# * :format - Calling view_model.render_template('template') will render the haml
|
216
|
+
# template, calling view_model.render_template('template', :format => :text) will render
|
217
|
+
# the text erb template.
|
218
|
+
# * All other options are passed on to the render call. I.e. if you want to specify locals you can call
|
219
|
+
# view_model.render_template(:template, :locals => { :name => :value })
|
220
|
+
# * If no format is given, it will render the default format, which is (currently) html.
|
221
|
+
#
|
222
|
+
def render_template name, options = {}
|
223
|
+
render RenderOptions::Template.new(name, options)
|
224
|
+
end
|
225
|
+
|
226
|
+
protected
|
227
|
+
|
228
|
+
# CaptureHelper needs this.
|
229
|
+
#
|
230
|
+
attr_accessor :output_buffer
|
231
|
+
|
232
|
+
# Internal render method that uses the options to get a view instance
|
233
|
+
# and then referring to its class for rendering.
|
234
|
+
#
|
235
|
+
def render options
|
236
|
+
options.view_model = self
|
237
|
+
|
238
|
+
determine_and_set_format options
|
239
|
+
|
240
|
+
self.class.render view_instance, options
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns a view instance for render_xxx.
|
244
|
+
#
|
245
|
+
# TODO Try getting a view instance from the controller.
|
246
|
+
#
|
247
|
+
def view_instance
|
248
|
+
# view = if controller.response.template
|
249
|
+
# controller.response.template
|
250
|
+
# else
|
251
|
+
View.new controller, master_helper_module
|
252
|
+
# end
|
253
|
+
|
254
|
+
# view.extend Extensions::View
|
255
|
+
end
|
256
|
+
|
257
|
+
# Determines what format to use for rendering.
|
258
|
+
#
|
259
|
+
# Note: Uses the template format of the view model instance
|
260
|
+
# if none is explicitly set in the options.
|
261
|
+
# This propagates the format to further render_xxx calls.
|
262
|
+
#
|
263
|
+
def determine_and_set_format options
|
264
|
+
options.format = @template_format = options.format || @template_format
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Base Module for ViewModels.
|
2
|
+
#
|
3
|
+
module ViewModels
|
4
|
+
|
5
|
+
# Extracts controllers for a living from unsuspecting views.
|
6
|
+
#
|
7
|
+
class ControllerExtractor
|
8
|
+
|
9
|
+
attr_reader :context
|
10
|
+
|
11
|
+
def initialize context
|
12
|
+
@context = context
|
13
|
+
end
|
14
|
+
|
15
|
+
# Extracts a controller from the context.
|
16
|
+
#
|
17
|
+
def extract
|
18
|
+
context = self.context
|
19
|
+
context.respond_to?(:controller) ? context.controller : context
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Base Module for ViewModels.
|
2
|
+
#
|
3
|
+
module ViewModels
|
4
|
+
|
5
|
+
# A simple path store. Designed to remove a bit of complexity from the base view model.
|
6
|
+
#
|
7
|
+
# Use it to install an instance in the metaclass.
|
8
|
+
#
|
9
|
+
class PathStore
|
10
|
+
|
11
|
+
attr_reader :view_model_class
|
12
|
+
|
13
|
+
def initialize view_model_class
|
14
|
+
@view_model_class = view_model_class
|
15
|
+
@name_path_mapping = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Install in the metaclass (as an example).
|
19
|
+
#
|
20
|
+
def self.install_in klass
|
21
|
+
klass.path_store = PathStore.new klass
|
22
|
+
end
|
23
|
+
|
24
|
+
# Cache the result of the rendering.
|
25
|
+
#
|
26
|
+
def cached options, &block
|
27
|
+
prepare options.path_key
|
28
|
+
result = block.call
|
29
|
+
save options and result if result
|
30
|
+
end
|
31
|
+
|
32
|
+
# Prepare the key for the next storing procedure.
|
33
|
+
#
|
34
|
+
# Note: If this is nil, the store will not save the path.
|
35
|
+
#
|
36
|
+
def prepare key
|
37
|
+
@key = key
|
38
|
+
end
|
39
|
+
|
40
|
+
# Saves the options for the prepared key.
|
41
|
+
#
|
42
|
+
def save options
|
43
|
+
self[@key] = options.file
|
44
|
+
end
|
45
|
+
|
46
|
+
# Does not save values for nil keys.
|
47
|
+
#
|
48
|
+
def []= key, path
|
49
|
+
return if key.nil?
|
50
|
+
@name_path_mapping[key] ||= path
|
51
|
+
end
|
52
|
+
|
53
|
+
# Simple [] delegation.
|
54
|
+
#
|
55
|
+
def [] key
|
56
|
+
@name_path_mapping[key]
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module ViewModels
|
2
|
+
|
3
|
+
# Container object for render options.
|
4
|
+
#
|
5
|
+
module RenderOptions
|
6
|
+
|
7
|
+
# Hold a number of options for rendering.
|
8
|
+
#
|
9
|
+
class Base
|
10
|
+
|
11
|
+
attr_accessor :path, :name, :prefix, :file, :view_model, :format
|
12
|
+
|
13
|
+
def initialize prefix, name, options
|
14
|
+
@prefix = prefix
|
15
|
+
@options = options
|
16
|
+
self.template_name = deoptionize name
|
17
|
+
@format = @options.delete :format
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generate a suitable error message for the error options.
|
21
|
+
#
|
22
|
+
def error_message
|
23
|
+
"'#{error_path}#{name}' with #{error_format}"
|
24
|
+
end
|
25
|
+
def error_path
|
26
|
+
path = self.path
|
27
|
+
path ? "#{path}/" : ""
|
28
|
+
end
|
29
|
+
def error_format
|
30
|
+
format = self.format
|
31
|
+
format ? "format #{format}" : "default format"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Used when rendering.
|
35
|
+
#
|
36
|
+
def to_render_options
|
37
|
+
@options[:locals] ||= {}
|
38
|
+
@options[:locals].reverse_merge! :view_model => view_model
|
39
|
+
@options.reverse_merge :file => file
|
40
|
+
end
|
41
|
+
|
42
|
+
def format! view
|
43
|
+
view.template_format = @format if @format
|
44
|
+
end
|
45
|
+
|
46
|
+
# Used for caching.
|
47
|
+
#
|
48
|
+
def path_key
|
49
|
+
[self.path, self.name, self.format]
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# TODO rewrite
|
55
|
+
#
|
56
|
+
def template_name= template_name
|
57
|
+
template_name.to_s.include?('/') ? specific_path(template_name) : incomplete_path(template_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
#
|
62
|
+
def deoptionize template_name
|
63
|
+
if template_name.kind_of?(Hash)
|
64
|
+
@options.merge! template_name
|
65
|
+
@options.delete :partial
|
66
|
+
else
|
67
|
+
template_name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
#
|
73
|
+
def specific_path name
|
74
|
+
self.path = File.dirname name
|
75
|
+
self.name_with_prefix = File.basename name
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
#
|
80
|
+
def incomplete_path name
|
81
|
+
self.path = nil
|
82
|
+
self.name_with_prefix = name
|
83
|
+
end
|
84
|
+
|
85
|
+
def name_with_prefix= name
|
86
|
+
self.name = "#{self.prefix}#{name}"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# A specific container for partial rendering.
|
92
|
+
#
|
93
|
+
class Partial < Base
|
94
|
+
def initialize name, options = {}
|
95
|
+
super :'_', name, options
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# A specific container for template rendering.
|
100
|
+
#
|
101
|
+
class Template < Base
|
102
|
+
def initialize name, options = {}
|
103
|
+
super nil, name, options
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ViewModels
|
2
|
+
# View model specific view.
|
3
|
+
#
|
4
|
+
class View < ActionView::Base
|
5
|
+
|
6
|
+
# Include the helpers from the view model.
|
7
|
+
#
|
8
|
+
def initialize controller, master_helper_module
|
9
|
+
metaclass.send :include, master_helper_module
|
10
|
+
super controller.class.view_paths, {}, controller
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
#
|
15
|
+
def render_with options
|
16
|
+
render options.to_render_options
|
17
|
+
end
|
18
|
+
|
19
|
+
# Finds the template in the view paths at the given path, with its format.
|
20
|
+
#
|
21
|
+
def find_template path
|
22
|
+
view_paths.find_template path, template_format rescue nil
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/view_models.rb
ADDED