view_models 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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