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
data/CHANGELOG
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
1.5.2 - 25.02.10 - floere: The capture method - if included per Capturehelper - works now in view models.
|
2
|
+
1.5.1 - 22.02.10 - floere: Renamed ViewModels::Helper to ViewModels::Helpers.
|
3
|
+
1.5.0 - 20.02.10 - floere: format propagates through collection rendering
|
4
|
+
render_as, new sig: :partial => to explicitly define the partial. (Syntactic Discouragement)
|
5
|
+
Added: render_with, an alias of render_as.
|
6
|
+
Added: render_template, to render non-partials.
|
7
|
+
1.4.2 - 10.02.10 - floere: render_as does not check the deprecated API anymore. Use the new options
|
8
|
+
form to pass a format.
|
9
|
+
- floere: MissingViewModelError is not thrown anymore.
|
10
|
+
- floere: include ViewModels::Extensions::ActiveRecord in your Project view model to
|
11
|
+
give id, dom_id, to_param capabilities to your view_models. Or include in a
|
12
|
+
subclass.
|
13
|
+
1.4.0 - 08.02.10 - floere: Changed Api of ViewModels::Base#render_as to go up through the view model
|
14
|
+
class hierarchy to find a partial.
|
15
|
+
1.3.0 - 29.01.10 - andi: Changed Api of ViewModels::Base#render_as to take an options hash. Now supports
|
16
|
+
passing locals to the render call.
|
17
|
+
1.2.0 - 13.01.10 - floere: Simple generator written.
|
18
|
+
1.1.0 - 30.06.09 - floere: Rewrite: Representer => ViewModel.
|
19
|
+
1.0.2 - 08.08.08 - floere: Renamed Representers::Helper::Rails to Representers::Helper::Rails.
|
20
|
+
- floere: Added Representers::Helpers::View for conveniently integrating common view-helpers.
|
21
|
+
- floere: Added controller_methods for RequestForgeryProtection in ViewModels::Base.
|
22
|
+
- floere: Added methods for id and dom_id in Representers::ActiveRecord.
|
23
|
+
- floere: Included ActionController::Helpers in Representers::Base and removed own helper-method.
|
24
|
+
- floere: Changed visibility of method view_instance in Representers::Base from private to protected.
|
25
|
+
1.0.1 - 24.07.08 - floere: Rewrite: Presenter => Representer.
|
26
|
+
1.0.0 - May 2008 - floere: Initial commit.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Florian Hanke & Kaspar Schiess
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
h1. ViewModels for Rails
|
2
|
+
|
3
|
+
h2. Previous contributors
|
4
|
+
|
5
|
+
Code:
|
6
|
+
|
7
|
+
"Kaspar":http://github.com/kschiess @http://github.com/kschiess@, for the first, and foundation-laying version.
|
8
|
+
|
9
|
+
"Niko":http://github.com/niko @http://github.com/niko@, for handling collections and better Helpers handling.
|
10
|
+
|
11
|
+
"Andi":http://github.com/andi @http://github.com/andi@, for refactoring it into a cleaner structure.
|
12
|
+
|
13
|
+
Inspiration:
|
14
|
+
|
15
|
+
"Severin":http://github.com/severin @http://github.com/severin@, for bits and pieces here and there.
|
16
|
+
|
17
|
+
"rainhead":http://github.com/rainhead @http://github.com/rainhead@, for the idea to subclass ActionView::Base.
|
18
|
+
|
19
|
+
Important note: These guys rock! :)
|
20
|
+
|
21
|
+
h2. Installation
|
22
|
+
|
23
|
+
@script/plugin install git://github.com/floere/view_models.git@
|
24
|
+
|
25
|
+
h2. Description
|
26
|
+
|
27
|
+
A possible view_model solution, i.e. no view logic in model code.
|
28
|
+
|
29
|
+
h2. Feedback
|
30
|
+
|
31
|
+
Ask/Write florian.hanke@gmail.com if you have questions/feedback, thanks! :)
|
32
|
+
Fork if you have improvements. Send me a pull request, it is much appreciated.
|
33
|
+
|
34
|
+
h2. Problem
|
35
|
+
|
36
|
+
Display Methods are not well placed either in
|
37
|
+
* models: Violation of the MVC principle.
|
38
|
+
* helpers: No Polymorphism.
|
39
|
+
|
40
|
+
h2. Solution
|
41
|
+
|
42
|
+
A thin proxy layer over a model, with access to the controller, used by the view or controller.
|
43
|
+
|
44
|
+
@The view@ -> to @the view model@ which in turn -> @the model@ and -> @the controller@
|
45
|
+
|
46
|
+
h2. Examples & What you can do
|
47
|
+
|
48
|
+
h3. A quick one
|
49
|
+
|
50
|
+
In the view:
|
51
|
+
<pre><code>user = view_model_for @user
|
52
|
+
%h1= "You, #{user.full_name}, the user"
|
53
|
+
%h2 This is how you look as a search result:
|
54
|
+
= user.render_as :result
|
55
|
+
%h2 This is how you look as a Vcard:
|
56
|
+
= user.render_as :vcard</code></pre>
|
57
|
+
In the view model:
|
58
|
+
<pre><code>class ViewModels::User < ViewModels::Project
|
59
|
+
model_reader :first_name, :last_name
|
60
|
+
def full_name
|
61
|
+
"#{last_name}, #{first_name}"
|
62
|
+
end
|
63
|
+
end</code></pre>
|
64
|
+
In the model:
|
65
|
+
<pre><code>class User < ActiveRecord::Base
|
66
|
+
end</code></pre>
|
67
|
+
Also, there are two partials in @app/views/view_models/user/@, @_result.html.haml@ and @_vcard.html.haml@ that define how the result of @user.render_as :result@ and @user.render_as :vcard@ look. The ViewModel can be accessed inside the view by using the local variable @view_model@.
|
68
|
+
|
69
|
+
h3. Getting a view_model in a view or a controller.
|
70
|
+
|
71
|
+
Call view_model_for: @view_model_instance = view_model_for model_instance@
|
72
|
+
By convention, uses @ViewModels::Model::Class::Name@, thus prefixing @ViewModels::@ to the model class name.
|
73
|
+
|
74
|
+
Note: You can override @specific_view_model_class_for@ to change the mapping of model to class, or make it dynamic.
|
75
|
+
|
76
|
+
h3. Getting a collection view model in a view.
|
77
|
+
|
78
|
+
The collection view_model renders each of the given items with its view_model.
|
79
|
+
|
80
|
+
Call collection_view_model_for: @collection_view_model_instance = collection_view_model_for enumerable_containing_model_instances@
|
81
|
+
|
82
|
+
Rendering a list: @collection_view_model_instance.list@
|
83
|
+
|
84
|
+
Rendering a collection: @collection_view_model_instance.collection@
|
85
|
+
|
86
|
+
Rendering a table: @collection_view_model_instance.table@
|
87
|
+
|
88
|
+
Rendering a pagination: @collection_view_model_instance.pagination@
|
89
|
+
Note: Only works if the passed parameter for @collection_view_model_for@ is a @PaginationEnumeration@.
|
90
|
+
|
91
|
+
Important note:
|
92
|
+
As of yet it is needed to copy the templates/views/view_models/collection
|
93
|
+
directory to the corresponding location in app/views/view_models/collection.
|
94
|
+
This is only needed if you wish to use the collection view model.
|
95
|
+
The collections are automatically copied if you use the generator.
|
96
|
+
|
97
|
+
Note: Rewrite the collection templates as needed, they are rather basic.
|
98
|
+
|
99
|
+
h3. Writing filtered delegate methods on the view model.
|
100
|
+
|
101
|
+
Will create two delegate methods first_name and last_name that delegate to the model: @model_reader :first_name, :last_name@
|
102
|
+
|
103
|
+
Will create a description delegate method that filters the model value through h: @model_reader :description, :filter_through => :h@
|
104
|
+
|
105
|
+
Will create a description delegate method that filters the model value through first textilize, then h: @model_reader :description, :filter_through => [:textilize, :h]@
|
106
|
+
|
107
|
+
Will create both a first_name and last_name delegate method that filters the model value through first textilize, then h: @model_reader :first_name, :last_name, :filter_through => [:textilize, :h]@
|
108
|
+
Note: Filter methods can be any method on the view_model with arity 1.
|
109
|
+
|
110
|
+
h3. Rendering view model templates
|
111
|
+
|
112
|
+
Use @render_as(template_name, options)@.
|
113
|
+
|
114
|
+
Gets a @ViewModels::Model::Class@ instance: @view_model = view_model_for Model::Class.new@
|
115
|
+
|
116
|
+
Gets a @ViewModels::<model_instance.class.name>@ instance: @view_model = view_model_for model_instance@
|
117
|
+
|
118
|
+
Renders the 'example' partial in view_models/model/class: @view_model.render_as :example@
|
119
|
+
Note: Renders a format depending on the request. ../index.text will render example.text.erb.
|
120
|
+
|
121
|
+
Renders the 'example.text.erb' partial in view_models/model/class: @view_model.render_as :example, :format => :text@
|
122
|
+
Note: If the partial cannot be found, it will traverse the view model hierarchy upwards to find a partial template.
|
123
|
+
|
124
|
+
Locals can be passed through as usual: @view_model.render_as :example, :format => :text, :locals => { :name => value }@
|
125
|
+
|
126
|
+
h3. Rails Helpers in ViewModels
|
127
|
+
|
128
|
+
Use @helper@ as you would in the controller.
|
129
|
+
@helper ActionView::Helpers::UrlHelper@
|
130
|
+
@helper ApplicationHelper@
|
131
|
+
Note: It is helpful to create a superclass to all view models in the project with generally used helpers.
|
132
|
+
We use @ViewModels::Project@ a lot, for example. See example below.
|
133
|
+
|
134
|
+
h3. Controller Delegate Methods
|
135
|
+
|
136
|
+
Use @controller_method(*args)@.
|
137
|
+
|
138
|
+
Delegates current_user and logger on the view_model to the controller: @controller_method :current_user, :logger@
|
139
|
+
|
140
|
+
h2. The Generator
|
141
|
+
|
142
|
+
Generates view model class, spec, and views. Use as follows:
|
143
|
+
|
144
|
+
@script/generate view_models <view model class name>@
|
145
|
+
@script/generate view_models User@
|
146
|
+
|
147
|
+
@script/generate view_models <view model class name> <partials for render_as, separated by space>@
|
148
|
+
@script/generate view_models User compact extensive list_item table_item@
|
149
|
+
|
150
|
+
h2. One Big Fat Example
|
151
|
+
|
152
|
+
The following classes all have specs of course ;) But are not shown since they don't help the example.
|
153
|
+
|
154
|
+
@ViewModels@ superclass for this project.
|
155
|
+
|
156
|
+
We include all of Rails' helpers for the view models in this project.
|
157
|
+
Also, we include the @ApplicationHelper@.
|
158
|
+
|
159
|
+
We delegate @logger@ and @current_user@ calls in the view models to the active controller.
|
160
|
+
|
161
|
+
<pre>
|
162
|
+
<code>
|
163
|
+
class ViewModels::Project < ViewModels::Base
|
164
|
+
|
165
|
+
# Our ApplicationHelper.
|
166
|
+
#
|
167
|
+
helper ApplicationHelper
|
168
|
+
|
169
|
+
# We want to be able to call view_model_for in our view_models.
|
170
|
+
#
|
171
|
+
helper ViewModels::Helpers::Rails
|
172
|
+
|
173
|
+
# Include all common view helpers.
|
174
|
+
#
|
175
|
+
helper ViewModels::Helpers::View
|
176
|
+
|
177
|
+
# We want to be able to use id, dom_id, to_param on the view model.
|
178
|
+
#
|
179
|
+
# Note: Overrides the standard dom_id method from the RecordIdentificationHelper.
|
180
|
+
#
|
181
|
+
include ViewModels::Extensions::ActiveRecord
|
182
|
+
|
183
|
+
controller_method :logger, :current_user
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
# All items have a description that needs to be filtered by textilize.
|
188
|
+
#
|
189
|
+
class ViewModels::Item < ViewModels::Project
|
190
|
+
model_reader :description, :filter_through => :textilize
|
191
|
+
# Use price in the view as follows:
|
192
|
+
# = view_model.price - will display e.g. 16.57 CHF, since it is filtered first through localize_currency
|
193
|
+
model_reader :price, :filter_through => :localize_currency
|
194
|
+
|
195
|
+
# Converts a database price tag to the users chosen value, with the users preferred currency appended.
|
196
|
+
# If the user is Swiss, localize_currency will convert 10 Euros to "16.57 CHF"
|
197
|
+
#
|
198
|
+
def localize_currency(price_in_euros)
|
199
|
+
converted_price = current_user.convert_price(price_in_euros)
|
200
|
+
"#{converted_price} #{current_user.currency.to_s}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# This class also has partial templates in the directory
|
205
|
+
# app/views/view_models/book
|
206
|
+
# that are called
|
207
|
+
# _cart_item.html.haml
|
208
|
+
# _cart_item.text.erb
|
209
|
+
#
|
210
|
+
# Call view_model_for on a book in the view or controller to get this view_model.
|
211
|
+
#
|
212
|
+
class ViewModels::Book < ViewModels::Item
|
213
|
+
model_reader :author, :title, :pages
|
214
|
+
model_reader :excerpt, :filter_through => :textilize
|
215
|
+
|
216
|
+
def header
|
217
|
+
content_tag(:h1, "#{author} – #{title}")
|
218
|
+
end
|
219
|
+
|
220
|
+
def full_description
|
221
|
+
content_tag(:p, "#{excerpt} #{description}", :class => 'description full')
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# This class also has partial templates in the directory
|
226
|
+
# app/views/view_models/toy
|
227
|
+
# that are called
|
228
|
+
# _cart_item.html.haml
|
229
|
+
# _cart_item.text.erb
|
230
|
+
#
|
231
|
+
# Call view_model_for on a toy in the view or controller to get this view_model.
|
232
|
+
#
|
233
|
+
class ViewModels::Toy < ViewModels::Item
|
234
|
+
model_reader :starting_age, :small_dangerous_parts
|
235
|
+
|
236
|
+
def obligatory_parental_warning
|
237
|
+
"Warning, this toy can only be used by kids ages #{starting_age} and up. Your department of health. Thank you."
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
</code>
|
242
|
+
</pre>
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
def require_if task, name
|
2
|
+
require name if ARGV[0] =~ %r{^#{task}}
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'rake'
|
6
|
+
require 'rake/testtask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
|
9
|
+
require 'spec/rake/spectask'
|
10
|
+
require 'spec/rake/verify_rcov'
|
11
|
+
|
12
|
+
require_if :metrics, 'metric_fu'
|
13
|
+
|
14
|
+
task :default => :spec
|
15
|
+
|
16
|
+
# run with rake spec
|
17
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
18
|
+
t.spec_opts = %w{--colour --format progress --loadby mtime --reverse}
|
19
|
+
t.spec_files = Dir.glob('spec/**/*_spec.rb')
|
20
|
+
t.warning = false
|
21
|
+
end
|
22
|
+
|
23
|
+
# run with rake rcov
|
24
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
25
|
+
t.spec_opts = %w{--colour --format progress --loadby mtime --reverse}
|
26
|
+
t.spec_files = Dir.glob('spec/**/*_spec.rb')
|
27
|
+
t.warning = false
|
28
|
+
t.rcov = true
|
29
|
+
puts "Open coverage/index.html for the rcov results."
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'jeweler'
|
34
|
+
Jeweler::Tasks.new do |gemspec|
|
35
|
+
gemspec.name = "view_models"
|
36
|
+
gemspec.summary = "A model proxy for Rails views. Helps you keep the representation of a model and the model itself separate."
|
37
|
+
gemspec.email = "florian.hanke@gmail.com"
|
38
|
+
gemspec.homepage = "http://floere.github.com/view_models"
|
39
|
+
gemspec.description = "The view models gem adds the missing R (Representation) to Rails' MVC. It provides simple proxy functionality for your models and thus helps you keep the model and view representations of a model separate, as it should be. Also, you can define helper methods on the (view) model instead of globally to keep them focused, more quickly understood and more easily testable. View Models also introduce hierarchical rendering for your hierarchical models. If the account view is not defined for the subclass FemaleUser, it checks if it is defined for User, for example, to see when there is no specific view, if there is a general view. So, in other words: Polymorphism not just in the model, but also in the view."
|
40
|
+
gemspec.authors = ["Florian Hanke", "Kaspar Schiess", "Niko Dittmann", "Andreas Schacke"]
|
41
|
+
gemspec.rdoc_options = ["--inline-source", "--charset=UTF-8"]
|
42
|
+
gemspec.files = FileList["[A-Z]*", "{generators,lib,rails,spec}/**/*"]
|
43
|
+
end
|
44
|
+
Jeweler::GemcutterTasks.new
|
45
|
+
rescue LoadError => e
|
46
|
+
puts "Jeweler not available (#{e}). Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
47
|
+
end
|
data/VERSION.yml
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
Usage:
|
2
|
+
script/generate view_models <view model class name>
|
3
|
+
script/generate view_models User
|
4
|
+
|
5
|
+
script/generate view_models <view model class name> <partials for render_as, separated by space>
|
6
|
+
script/generate view_models User compact extensive list_item table_item
|
@@ -0,0 +1 @@
|
|
1
|
+
View Model generated. Have fun!
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
<ul class="collection"><%- collection.each do |item| -%><%- view_model = view_model_for item -%><li><%- options = template_format ? { :format => template_format } : {} -%><%= view_model.render_as template_name, options -%></li><%= separator unless item == collection.last %><%- end -%></ul>
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<%- collection.each do |item| -%>
|
2
|
+
<%- view_model = view_model_for item -%>
|
3
|
+
<%- options = (template_format ? { :format => template_format } : {}) -%>
|
4
|
+
<%= view_model.render_as template_name, options -%>
|
5
|
+
<%= separator unless item == collection.entries.last -%>
|
6
|
+
<%- end -%>
|
@@ -0,0 +1 @@
|
|
1
|
+
<ol class="collection"><%- collection.each do |item| -%><%- view_model = view_model_for item -%><li><%- options = (template_format ? { :format => template_format } : {}) -%><%= view_model.render_as template_name, options -%></li><%= separator unless item == collection.last %><%- end -%></ol>
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<% collection.each do |item| -%>
|
2
|
+
<% view_model = view_model_for item -%>
|
3
|
+
<%- options = (template_format ? { :format => template_format } : {}) -%>
|
4
|
+
<%= view_model.render_as template_name, options -%>
|
5
|
+
<%= separator || '\n' unless item == collection.entries.last -%>
|
6
|
+
<% end -%>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
- if collection.page_count > 1
|
2
|
+
%p
|
3
|
+
- if collection.previous_page?
|
4
|
+
= link_to 'zurückblättern', :page => collection.page-1
|
5
|
+
- 1.upto(collection.page_count) do |page|
|
6
|
+
- if page == collection.page
|
7
|
+
= page.to_s
|
8
|
+
- else
|
9
|
+
= link_to page, :page => page
|
10
|
+
= separator unless collection.last_page?
|
11
|
+
- if collection.next_page?
|
12
|
+
= link_to 'vorblättern', :page => collection.page+1
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<%- width = 66 -%>
|
2
|
+
<%= '-'*width %>
|
3
|
+
<% collection.each do |item| -%>
|
4
|
+
<% view_model = view_model_for(item, self) -%>
|
5
|
+
<%- result = view_model.render_as(template_name) -%>
|
6
|
+
<%= '|' + "%-#{width}s"%(result[0,width]) + '|' -%>
|
7
|
+
<%- p ('|' + "%-#{width}s"%(result[0,width]) + '|') %>
|
8
|
+
<%= separator || ">#{'-'*(width-2)}<" unless item == collection.entries.last %>
|
9
|
+
<% end -%>
|
10
|
+
<%= '-'*width %>
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class ViewModelsGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
|
5
|
+
# Check for class naming collisions.
|
6
|
+
#
|
7
|
+
m.class_collisions "ViewModels::#{class_name}"
|
8
|
+
|
9
|
+
# ViewModels
|
10
|
+
#
|
11
|
+
m.directory 'app/view_models'
|
12
|
+
m.template "view_models/view_model.rb", "app/view_models/#{file_name}.rb"
|
13
|
+
|
14
|
+
# Specs
|
15
|
+
#
|
16
|
+
m.directory "spec/app/view_models"
|
17
|
+
m.template "spec/view_model_spec.rb", "spec/app/view_models/#{file_name}_spec.rb"
|
18
|
+
|
19
|
+
# Views
|
20
|
+
#
|
21
|
+
m.directory "app/views/view_models"
|
22
|
+
m.directory "app/views/view_models/#{file_name}"
|
23
|
+
actions << 'list_item' if actions.empty?
|
24
|
+
actions.each do |action|
|
25
|
+
m.template "views/_empty.html.haml", "app/views/view_models/#{file_name}/_#{action}.html.haml"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Copy collection views.
|
29
|
+
#
|
30
|
+
m.directory "app/views/view_models/collection"
|
31
|
+
m.file "views/view_models/collection/_collection.html.haml", "app/views/view_models/collection/_collection.html.haml"
|
32
|
+
m.file "views/view_models/collection/_collection.text.erb", "app/views/view_models/collection/_collection.text.erb"
|
33
|
+
m.file "views/view_models/collection/_list.html.haml", "app/views/view_models/collection/_list.html.haml"
|
34
|
+
m.file "views/view_models/collection/_list.text.erb", "app/views/view_models/collection/_list.text.erb"
|
35
|
+
m.file "views/view_models/collection/_pagination.html.haml", "app/views/view_models/collection/_pagination.html.haml"
|
36
|
+
m.file "views/view_models/collection/_pagination.text.erb", "app/views/view_models/collection/_pagination.text.erb"
|
37
|
+
m.file "views/view_models/collection/_table.html.haml", "app/views/view_models/collection/_table.html.haml"
|
38
|
+
m.file "views/view_models/collection/_table.text.erb", "app/views/view_models/collection/_table.text.erb"
|
39
|
+
|
40
|
+
# Show README.
|
41
|
+
#
|
42
|
+
m.readme "README"
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Makes certain AR Model methods available to the view model.
|
2
|
+
#
|
3
|
+
# Useful when the model is an AR Model.
|
4
|
+
#
|
5
|
+
module ViewModels
|
6
|
+
module Extensions
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
delegate :id, :to_param, :to => :model
|
10
|
+
|
11
|
+
# Delegate to the action controller record identifier.
|
12
|
+
#
|
13
|
+
def dom_id
|
14
|
+
ActionController::RecordIdentifier.dom_id model
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Makes model reader installation, including filtering in-between possible.
|
2
|
+
#
|
3
|
+
#
|
4
|
+
module ViewModels
|
5
|
+
module Extensions
|
6
|
+
module ModelReader
|
7
|
+
|
8
|
+
# Define a reader for a model attribute. Acts as a filtered delegation to the model.
|
9
|
+
#
|
10
|
+
# You may specify a :filter_through option that is either a symbol or an array of symbols. The return value
|
11
|
+
# from the model will be filtered through the functions (arity 1) and then passed back to the receiver.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# model_reader :foobar # same as delegate :foobar, :to => :model
|
16
|
+
# model_reader :foobar, :filter_through => :h # html escape foobar
|
17
|
+
# model_reader :foobar, :filter_through => [:textilize, :h] # first textilize, then html escape
|
18
|
+
#
|
19
|
+
def model_reader *attributes_and_options
|
20
|
+
options = Options.new *attributes_and_options
|
21
|
+
FilteredDelegationInstaller.new(self, options).install
|
22
|
+
end
|
23
|
+
|
24
|
+
# Bundles the model reader options and extracts the relevant structured data.
|
25
|
+
#
|
26
|
+
class Options
|
27
|
+
|
28
|
+
attr_reader :attributes, :filters
|
29
|
+
|
30
|
+
def initialize *attributes_and_options
|
31
|
+
split attributes_and_options
|
32
|
+
end
|
33
|
+
|
34
|
+
# Extract filter_through options from the options hash if there are any.
|
35
|
+
#
|
36
|
+
def split options
|
37
|
+
@filters = if options.last.kind_of?(Hash)
|
38
|
+
[*(options.pop[:filter_through])].reverse
|
39
|
+
else
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
@attributes = options
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_a
|
46
|
+
[attributes, filters]
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# The filtered delegation installer installs delegators on the target
|
52
|
+
# that are filtered.
|
53
|
+
#
|
54
|
+
class FilteredDelegationInstaller
|
55
|
+
|
56
|
+
attr_reader :target, :attributes, :filters
|
57
|
+
|
58
|
+
def initialize target, options
|
59
|
+
@target, @attributes, @filters = target, *options
|
60
|
+
end
|
61
|
+
|
62
|
+
# Install install
|
63
|
+
#
|
64
|
+
def install
|
65
|
+
attributes.each { |attribute| install_reader(attribute) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# Install a reader for the given name with the given filters.
|
69
|
+
#
|
70
|
+
# Example:
|
71
|
+
# # Installs a reader for model.attribute
|
72
|
+
# #
|
73
|
+
# * install_reader :attribute
|
74
|
+
#
|
75
|
+
def install_reader attribute
|
76
|
+
target.class_eval reader_definition_for(attribute)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Defines a reader for the given model attribute and filtering
|
80
|
+
# through the given filters.
|
81
|
+
#
|
82
|
+
# Note: The filters are applied from last to first element.
|
83
|
+
#
|
84
|
+
def reader_definition_for attribute
|
85
|
+
"def #{attribute}; #{filtered_left_parentheses}model.#{attribute}#{right_parentheses}; end"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Combines left parentheses and filters.
|
89
|
+
#
|
90
|
+
def filtered_left_parentheses
|
91
|
+
filters.zip(left_parentheses).to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
# Generates the needed amount of parentheses to match the left parentheses.
|
95
|
+
#
|
96
|
+
def right_parentheses
|
97
|
+
')' * filters.size
|
98
|
+
end
|
99
|
+
|
100
|
+
# Generates an array of left parentheses with
|
101
|
+
# length <amount of filters>
|
102
|
+
# Example:
|
103
|
+
# # 4 Filters
|
104
|
+
# #
|
105
|
+
# left_parentheses # => ['(', '(', '(', '(']
|
106
|
+
#
|
107
|
+
def left_parentheses
|
108
|
+
['('] * filters.size
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Not used at the moment
|
2
|
+
#
|
3
|
+
# Extends a standard ActionView::Base view with methods
|
4
|
+
# needed by the view models.
|
5
|
+
#
|
6
|
+
module ViewModels
|
7
|
+
module Extensions
|
8
|
+
module View
|
9
|
+
|
10
|
+
#
|
11
|
+
#
|
12
|
+
def render_with options
|
13
|
+
render options.to_render_options
|
14
|
+
end
|
15
|
+
|
16
|
+
# Finds the template in the view paths at the given path, with its format.
|
17
|
+
#
|
18
|
+
def find_template path
|
19
|
+
view_paths.find_template path, template_format rescue nil
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|