view_models 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/CHANGELOG +26 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +242 -0
  4. data/Rakefile +47 -0
  5. data/VERSION.yml +4 -0
  6. data/generators/view_models/USAGE +6 -0
  7. data/generators/view_models/templates/README +1 -0
  8. data/generators/view_models/templates/spec/view_model_spec.rb +7 -0
  9. data/generators/view_models/templates/view_models/view_model.rb +5 -0
  10. data/generators/view_models/templates/views/_empty.html.haml +0 -0
  11. data/generators/view_models/templates/views/view_models/collection/_collection.html.erb +1 -0
  12. data/generators/view_models/templates/views/view_models/collection/_collection.html.haml +8 -0
  13. data/generators/view_models/templates/views/view_models/collection/_collection.text.erb +6 -0
  14. data/generators/view_models/templates/views/view_models/collection/_list.html.erb +1 -0
  15. data/generators/view_models/templates/views/view_models/collection/_list.html.haml +7 -0
  16. data/generators/view_models/templates/views/view_models/collection/_list.text.erb +6 -0
  17. data/generators/view_models/templates/views/view_models/collection/_pagination.html.haml +12 -0
  18. data/generators/view_models/templates/views/view_models/collection/_pagination.text.erb +3 -0
  19. data/generators/view_models/templates/views/view_models/collection/_table.html.haml +5 -0
  20. data/generators/view_models/templates/views/view_models/collection/_table.text.erb +10 -0
  21. data/generators/view_models/view_models_generator.rb +47 -0
  22. data/lib/extensions/active_record.rb +19 -0
  23. data/lib/extensions/model_reader.rb +115 -0
  24. data/lib/extensions/view.rb +24 -0
  25. data/lib/helpers/collection.rb +124 -0
  26. data/lib/helpers/rails.rb +59 -0
  27. data/lib/helpers/view.rb +22 -0
  28. data/lib/view_models/base.rb +268 -0
  29. data/lib/view_models/controller_extractor.rb +24 -0
  30. data/lib/view_models/path_store.rb +61 -0
  31. data/lib/view_models/render_options.rb +109 -0
  32. data/lib/view_models/view.rb +26 -0
  33. data/lib/view_models.rb +3 -0
  34. data/rails/init.rb +18 -0
  35. data/spec/integration/integration_spec.rb +269 -0
  36. data/spec/integration/models/sub_subclass.rb +14 -0
  37. data/spec/integration/models/subclass.rb +3 -0
  38. data/spec/integration/view_models/project.rb +14 -0
  39. data/spec/integration/view_models/sub_subclass.rb +42 -0
  40. data/spec/integration/view_models/subclass.rb +1 -0
  41. data/spec/integration/views/view_models/collection/_collection.html.erb +1 -0
  42. data/spec/integration/views/view_models/collection/_collection.text.erb +6 -0
  43. data/spec/integration/views/view_models/collection/_list.html.erb +1 -0
  44. data/spec/integration/views/view_models/collection/_list.text.erb +6 -0
  45. data/spec/integration/views/view_models/sub_subclass/_capture_in_template.erb +2 -0
  46. data/spec/integration/views/view_models/sub_subclass/_capture_in_view_model.erb +3 -0
  47. data/spec/integration/views/view_models/sub_subclass/_collection_example.html.erb +3 -0
  48. data/spec/integration/views/view_models/sub_subclass/_collection_example.text.erb +3 -0
  49. data/spec/integration/views/view_models/sub_subclass/_collection_item.html.erb +1 -0
  50. data/spec/integration/views/view_models/sub_subclass/_collection_item.text.erb +1 -0
  51. data/spec/integration/views/view_models/sub_subclass/_exists.erb +1 -0
  52. data/spec/integration/views/view_models/sub_subclass/_exists.html.erb +1 -0
  53. data/spec/integration/views/view_models/sub_subclass/_exists.text.erb +1 -0
  54. data/spec/integration/views/view_models/sub_subclass/_exists_in_both.erb +1 -0
  55. data/spec/integration/views/view_models/sub_subclass/_inner.also_explicit.erb +1 -0
  56. data/spec/integration/views/view_models/sub_subclass/_inner.nesting.erb +1 -0
  57. data/spec/integration/views/view_models/sub_subclass/_list_example.html.erb +3 -0
  58. data/spec/integration/views/view_models/sub_subclass/_list_example.text.erb +3 -0
  59. data/spec/integration/views/view_models/sub_subclass/_list_item.html.erb +1 -0
  60. data/spec/integration/views/view_models/sub_subclass/_list_item.text.erb +1 -0
  61. data/spec/integration/views/view_models/sub_subclass/_outer.explicit.erb +1 -0
  62. data/spec/integration/views/view_models/sub_subclass/_outer.nesting.erb +1 -0
  63. data/spec/integration/views/view_models/sub_subclass/_part_that_is_dependent_on_the_view_model.erb +1 -0
  64. data/spec/integration/views/view_models/sub_subclass/show.html.erb +1 -0
  65. data/spec/integration/views/view_models/sub_subclass/show.text.erb +1 -0
  66. data/spec/integration/views/view_models/subclass/_exists_in_both.erb +1 -0
  67. data/spec/integration/views/view_models/subclass/_no_sub_subclass.erb +1 -0
  68. data/spec/integration/views/view_models/subclass/_not_found_in_sub_subclass.erb +1 -0
  69. data/spec/lib/extensions/active_record_spec.rb +31 -0
  70. data/spec/lib/extensions/model_reader_spec.rb +93 -0
  71. data/spec/lib/helpers/collection_spec.rb +196 -0
  72. data/spec/lib/helpers/rails_spec.rb +88 -0
  73. data/spec/lib/helpers/view_spec.rb +20 -0
  74. data/spec/lib/view_models/base_spec.rb +102 -0
  75. data/spec/lib/view_models/view_spec.rb +9 -0
  76. data/spec/spec_helper.rb +14 -0
  77. data/spec/spec_helper_extensions.rb +13 -0
  78. 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,4 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 5
4
+ :patch: 2
@@ -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!
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe ViewModels::<%= class_name %> do
4
+
5
+
6
+
7
+ end
@@ -0,0 +1,5 @@
1
+ class ViewModels::<%= class_name %> < ViewModels::Project
2
+
3
+ # model_reader :icon, :filter_through => [:h]
4
+
5
+ end
@@ -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,8 @@
1
+ %ul.collection
2
+ - collection.each do |item|
3
+ - view_model = view_model_for item
4
+ %li
5
+ - options = (template_format ? { :format => template_format } : {})
6
+ = view_model.render_as template_name, options
7
+ = separator unless item == collection.entries.last
8
+
@@ -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,7 @@
1
+ %ol.collection
2
+ - collection.each do |item|
3
+ - view_model = view_model_for item
4
+ %li
5
+ - options = (template_format ? { :format => template_format } : {})
6
+ = view_model.render_as template_name, options
7
+ = separator unless item == collection.last
@@ -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,3 @@
1
+ <%- if collection.page_count > 1 -%>
2
+ <%= "Page #{collection.page} of #{collection.page_count}." -%>
3
+ <%- end -%>
@@ -0,0 +1,5 @@
1
+ %table.collection
2
+ - collection.each do |item|
3
+ %tr
4
+ - view_model = view_model_for item
5
+ = view_model.render_as template_name
@@ -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