fortitude 0.9.1-java → 0.9.2-java

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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +28 -22
  4. data/CHANGES.md +50 -0
  5. data/CONTRIBUTORS.md +1 -0
  6. data/doc/.gitignore +18 -0
  7. data/doc/Gemfile +21 -0
  8. data/doc/config.rb +92 -0
  9. data/doc/source/images/background.png +0 -0
  10. data/doc/source/images/middleman.png +0 -0
  11. data/doc/source/images/why/icon_button.png +0 -0
  12. data/doc/source/images/why/icon_button@2x.png +0 -0
  13. data/doc/source/images/why/modal_dialog@2x.png +0 -0
  14. data/doc/source/index.html.pcss +96 -0
  15. data/doc/source/index.html.rb +66 -0
  16. data/doc/source/javascripts/all.js +1 -0
  17. data/doc/source/javascripts/highlight.pack.js +1 -0
  18. data/doc/source/layouts/layout.rb +55 -0
  19. data/doc/source/portable/fortitude-bootstrap.rb +53 -0
  20. data/doc/source/shared/base.pcss +62 -0
  21. data/doc/source/shared/base.rb +30 -0
  22. data/doc/source/shared/common.rb +55 -0
  23. data/doc/source/shared/standard_page.rb +40 -0
  24. data/doc/source/stylesheets/_shared_prefix.scss +25 -0
  25. data/doc/source/stylesheets/all.css.scss +7 -0
  26. data/doc/source/stylesheets/basics.css.scss +20 -0
  27. data/doc/source/stylesheets/bootstrap_importer.css.scss +1 -0
  28. data/doc/source/stylesheets/highlight/arta.css +140 -0
  29. data/doc/source/stylesheets/highlight/ascetic.css +52 -0
  30. data/doc/source/stylesheets/highlight/atelier-dune.dark.css +95 -0
  31. data/doc/source/stylesheets/highlight/atelier-dune.light.css +95 -0
  32. data/doc/source/stylesheets/highlight/atelier-forest.dark.css +95 -0
  33. data/doc/source/stylesheets/highlight/atelier-forest.light.css +95 -0
  34. data/doc/source/stylesheets/highlight/atelier-heath.dark.css +95 -0
  35. data/doc/source/stylesheets/highlight/atelier-heath.light.css +95 -0
  36. data/doc/source/stylesheets/highlight/atelier-lakeside.dark.css +95 -0
  37. data/doc/source/stylesheets/highlight/atelier-lakeside.light.css +95 -0
  38. data/doc/source/stylesheets/highlight/atelier-seaside.dark.css +95 -0
  39. data/doc/source/stylesheets/highlight/atelier-seaside.light.css +95 -0
  40. data/doc/source/stylesheets/highlight/brown_paper.css +104 -0
  41. data/doc/source/stylesheets/highlight/brown_papersq.png +0 -0
  42. data/doc/source/stylesheets/highlight/codepen-embed.css +108 -0
  43. data/doc/source/stylesheets/highlight/color-brewer.css +168 -0
  44. data/doc/source/stylesheets/highlight/dark.css +104 -0
  45. data/doc/source/stylesheets/highlight/default.css +152 -0
  46. data/doc/source/stylesheets/highlight/docco.css +135 -0
  47. data/doc/source/stylesheets/highlight/far.css +111 -0
  48. data/doc/source/stylesheets/highlight/foundation.css +136 -0
  49. data/doc/source/stylesheets/highlight/github.css +124 -0
  50. data/doc/source/stylesheets/highlight/googlecode.css +147 -0
  51. data/doc/source/stylesheets/highlight/hybrid.css +170 -0
  52. data/doc/source/stylesheets/highlight/idea.css +125 -0
  53. data/doc/source/stylesheets/highlight/ir_black.css +109 -0
  54. data/doc/source/stylesheets/highlight/kimbie.dark.css +96 -0
  55. data/doc/source/stylesheets/highlight/kimbie.light.css +96 -0
  56. data/doc/source/stylesheets/highlight/magula.css +121 -0
  57. data/doc/source/stylesheets/highlight/mono-blue.css +69 -0
  58. data/doc/source/stylesheets/highlight/monokai.css +127 -0
  59. data/doc/source/stylesheets/highlight/monokai_sublime.css +154 -0
  60. data/doc/source/stylesheets/highlight/obsidian.css +153 -0
  61. data/doc/source/stylesheets/highlight/paraiso.dark.css +95 -0
  62. data/doc/source/stylesheets/highlight/paraiso.light.css +95 -0
  63. data/doc/source/stylesheets/highlight/pojoaque.css +107 -0
  64. data/doc/source/stylesheets/highlight/pojoaque.jpg +0 -0
  65. data/doc/source/stylesheets/highlight/railscasts.css +187 -0
  66. data/doc/source/stylesheets/highlight/rainbow.css +108 -0
  67. data/doc/source/stylesheets/highlight/school_book.css +112 -0
  68. data/doc/source/stylesheets/highlight/school_book.png +0 -0
  69. data/doc/source/stylesheets/highlight/solarized_dark.css +108 -0
  70. data/doc/source/stylesheets/highlight/solarized_light.css +108 -0
  71. data/doc/source/stylesheets/highlight/sunburst.css +164 -0
  72. data/doc/source/stylesheets/highlight/tomorrow-night-blue.css +95 -0
  73. data/doc/source/stylesheets/highlight/tomorrow-night-bright.css +94 -0
  74. data/doc/source/stylesheets/highlight/tomorrow-night-eighties.css +94 -0
  75. data/doc/source/stylesheets/highlight/tomorrow-night.css +95 -0
  76. data/doc/source/stylesheets/highlight/tomorrow.css +92 -0
  77. data/doc/source/stylesheets/highlight/vs.css +93 -0
  78. data/doc/source/stylesheets/highlight/xcode.css +158 -0
  79. data/doc/source/stylesheets/highlight/zenburn.css +118 -0
  80. data/doc/source/why/a_larger_view.html.rb +774 -0
  81. data/doc/source/why/a_simple_helper.html.rb +332 -0
  82. data/doc/source/why/building_a_rich_modal_dialog.html.rb +156 -0
  83. data/doc/source/why/commonality_and_inheritance.html.rb +564 -0
  84. data/doc/source/why/example_list.rb +60 -0
  85. data/doc/source/why/example_page.rb +116 -0
  86. data/doc/source/why/index.html.rb +86 -0
  87. data/doc/source/why/other_benefits.html.rb +110 -0
  88. data/fortitude.gemspec +6 -1
  89. data/lib/fortitude/doctypes/html4_tags_strict.rb +1 -0
  90. data/lib/fortitude/doctypes/html5.rb +1 -0
  91. data/lib/fortitude/method_templates/tag_method_template.rb.smpl +27 -14
  92. data/lib/fortitude/rails/helpers.rb +2 -2
  93. data/lib/fortitude/rendering_context.rb +10 -1
  94. data/lib/fortitude/tags/tag.rb +3 -2
  95. data/lib/fortitude/tags/tag_support.rb +8 -3
  96. data/lib/fortitude/tilt/fortitude_template.rb +6 -2
  97. data/lib/fortitude/version.rb +1 -1
  98. data/lib/fortitude/widget.rb +2 -0
  99. data/lib/fortitude/widget/convenience.rb +30 -0
  100. data/lib/fortitude/widget/files.rb +22 -11
  101. data/lib/fortitude/widget/needs.rb +5 -3
  102. data/spec/helpers/system_helpers.rb +1 -0
  103. data/spec/rails/development_mode_system_spec.rb +0 -1
  104. data/spec/rails/rendering_system_spec.rb +20 -1
  105. data/spec/rails/templates/rendering_system_spec/app/controllers/rendering_system_spec_controller.rb +13 -0
  106. data/spec/rails/templates/rendering_system_spec/app/views/rendering_system_spec/render_hash_subclass.rb +18 -0
  107. data/spec/rails/templates/rendering_system_spec/lib/my_hash.rb +5 -0
  108. data/spec/system/convenience_methods_system_spec.rb +166 -0
  109. data/spec/system/formatting_system_spec.rb +25 -1
  110. data/spec/system/tag_rendering_system_spec.rb +73 -0
  111. data/spec/system/tilt_system_spec.rb +31 -0
  112. data/spec/system/widget_class_from_spec.rb +20 -4
  113. metadata +91 -4
@@ -0,0 +1,564 @@
1
+ require 'source/why/example_page'
2
+
3
+ module Views
4
+ module Why
5
+ class CommonalityAndInheritance < Views::Why::ExamplePage
6
+ def example_intro
7
+ p {
8
+ text "In our last example, we saw how Fortitude allows you to share code among methods in the same "
9
+ text "view and how it lets you pass blocks to allow very flexible reuse and customization of that code."
10
+ }
11
+
12
+ p {
13
+ text "Here, we’ll explore how Fortitude allows you to use Ruby’s inheritance mechanism to easily factor "
14
+ text "out commonality among several related views. Using inheritance is one of the most powerful "
15
+ text "features of Fortitude."
16
+ }
17
+ end
18
+
19
+ def example_description
20
+ p {
21
+ text "Imagine we’re building the initial phases of a social network, and the time has come to create our "
22
+ text "main social feed. This feed is going to list various events: friends who have accepted our friend "
23
+ text "request, new posts in groups we follow, and even new features we’ve introduced to the site."
24
+ }
25
+
26
+ p {
27
+ text "This presents an interesting challenge for building views. The views for these feed items are likely "
28
+ text "to have quite a bit in common — yet almost all of it is going to be customized at "; em "some"
29
+ text " point. Still, let’s start simply, with just the three feed items mentioned above."
30
+ }
31
+
32
+ p {
33
+ text "First, the “accepted friend request” view:"
34
+ }
35
+
36
+ erb 'app/views/feed/items/accepted_friend_request.html.erb', <<-EOS
37
+ <div class="feed_item feed_item_accepted_friend_request" id="feed_item_<%= item.id %>">
38
+ <div class="feed_item_header">
39
+ <%= image_tag 'new_friend.png', :class => 'feed_item_image' %>
40
+ <h5><strong><%= item.accepting_user.first_name %></strong> accepted your friend request!</h5>
41
+ </div>
42
+
43
+ <div class="feed_item_body">
44
+ <p>
45
+ <a href="<%= profile_url(item.accepting_user) %>">
46
+ <img src="<%= item.accepting_user.profile_image %>">
47
+ <strong><%= item.accepting_user.full_name %></strong> is now your friend!
48
+ </a>
49
+ </p>
50
+ <p>You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!</p>
51
+ </div>
52
+
53
+ <div class="feed_item_footer">Posted at <%= item.created_at %></div>
54
+ </div>
55
+ EOS
56
+
57
+ p {
58
+ text "Now, the “new posts in groups you follow” view:"
59
+ }
60
+
61
+ erb 'app/views/feed/items/new_group_posts.html.erb', <<-EOS
62
+ <div class="feed_item feed_item_new_posts" id="feed_item_<%= item.id %>">
63
+ <div class="feed_item_header">
64
+ <%= image_tag 'new_post.png', :class => 'feed_item_image' %>
65
+ <h5>New posts in <%= item.new_groups.length %> groups!</h5>
66
+ </div>
67
+
68
+ <div class="feed_item_body">
69
+ <p>There have been new posts in the following groups:
70
+ <ul>
71
+ <% item.new_groups.each do |group| %>
72
+ <li><a href="<%= group_url(group) %>"><strong><%= group.title %></strong> (<%= group.new_posts %> new posts)</a></li>
73
+ <% end %>
74
+ </ul>
75
+ </p>
76
+ </div>
77
+
78
+ <div class="feed_item_footer">Posted at <%= item.created_at %></div>
79
+ </div>
80
+ EOS
81
+
82
+ p {
83
+ text "And, finally, the “new feature” view:"
84
+ }
85
+
86
+ erb 'app/views/feed/items/new_feature.html.erb', <<-EOS
87
+ <div class="feed_item feed_item_new_feature" item="feed_item_<%= item.id %>">
88
+ <div class="feed_item_header">
89
+ <%= image_tag 'features/custom_groups.png', :class => 'feed_item_image' %>
90
+ <h3>New features have launched!</h3>
91
+ </div>
92
+
93
+ <div class="feed_item_body">
94
+ <h5>We’ve been improving the site for your benefit!</h5>
95
+ <p>You can now create custom groups! We know you’ve all been asking for this feature for a while,
96
+ and it’s now ready for your use! <a href="<%= custom_groups_url %>">Click here</a> to create one!</p>
97
+ </div>
98
+
99
+ <div class="feed_item_footer">Posted at <%= item.created_at %></div>
100
+ </div>
101
+ EOS
102
+
103
+ p {
104
+ text "(Please note that we’ve simplified a lot of these views for our example. For example, you’d never "
105
+ text "want to show a user a raw Ruby "; code "Time"; text " value as we’ve done here.)"
106
+ }
107
+
108
+ p {
109
+ text "We can easily notice a lot of commonality in the views above:"
110
+ }
111
+
112
+ ul {
113
+ li {
114
+ strong "Container"; text ": All the views have a similar surrounding "; code "div"; text " that contains "
115
+ text "a per-view "; code "class"; text " and a per-feed-item "; code "id"; text "."
116
+ }
117
+ li {
118
+ strong "Header"; text ": Each view has a header that’s surrounded by a "; code "div"; text " that contains "
119
+ text "an image and some per-view HTML."
120
+ }
121
+ li {
122
+ strong "Body"; text ": Each view has a body that’s surrounded by a "; code "div"; text " that contains "
123
+ text "arbitrary HTML for that view — the real contents of our feed item."
124
+ }
125
+ li {
126
+ strong "Footer"; text ": Each view also has a footer that’s identical for all views."
127
+ }
128
+ }
129
+ end
130
+
131
+ def using_standard_engines
132
+ p {
133
+ text "Using traditional templating engines, what can we do here? Well, have the same tools at our disposal "
134
+ text "as always: partials and helpers. We can factor out commonality using these tools, "
135
+ text "potentially using "; code "capture"; text " to allow us to write HTML we pass into a view using "
136
+ text "ERb instead of Ruby string interpolation."
137
+ }
138
+
139
+ p {
140
+ text "Let’s first see what this looks like without using "; code "capture"; text ", and then we’ll see how "
141
+ text "or if using "; code "capture"; text " improves things."
142
+ }
143
+
144
+ h5 "Using Partials and Helpers"
145
+
146
+ p {
147
+ text "If we do our very best with this, here’s what we can end up with. We begin with a partial that "
148
+ text "starts the header of our view:"
149
+ }
150
+
151
+ erb 'app/views/feed/items/_feed_item_header.html.erb', <<-EOS
152
+ <div class="feed_item feed_item_<%= outer_css_class %>" id="feed_item_<%= item.id %>">
153
+ <div class="feed_item_header">
154
+ <%= image_tag header_icon_image, :class => 'feed_item_image' %>
155
+ EOS
156
+
157
+ p {
158
+ text "We have a really awkward situation going on between the header and the body, where we need to "
159
+ text "close the "; code "div"; text " we opened, then open another one. This seems a bit heavyweight for "
160
+ text "a partial, so let’s use a helper:"
161
+ }
162
+
163
+ ruby 'app/helpers/feed_helper.rb', <<-EOS
164
+ def feed_item_header_closer
165
+ "</div><div class=\"feed_item_body\">"
166
+ end
167
+ EOS
168
+
169
+ p {
170
+ text "Next, we have the body of the feed item, which we’ll leave to the individual view itself. Finally, "
171
+ text "we have the rest of the item template:"
172
+ }
173
+
174
+ erb 'app/views/feed/items/_feed_item_footer.html.erb', <<-EOS
175
+ </div>
176
+
177
+ <div class="feed_item_footer">Posted at <%= item.created_at %></div>
178
+ </div>
179
+ EOS
180
+
181
+ p {
182
+ text "Given these partials and helpers, what do our views now look like? First, let’s look at the "
183
+ text "“accepted friend request” view:"
184
+ }
185
+
186
+ erb 'app/views/feed/items/accepted_friend_request.html.erb', <<-EOS
187
+ <%= render :partial => 'feed_item_header',
188
+ :locals => { :outer_css_class => 'accepted_friend_request',
189
+ :item => item, :header_icon_image => 'new_friend.png' } %>
190
+ <h5><strong><%= item.accepting_user.first_name %></strong> accepted your friend request!</h5>
191
+ <%= feed_item_header_closer %>
192
+ <p>
193
+ <a href="<%= profile_url(item.accepting_user) %>">
194
+ <img src="<%= item.accepting_user.profile_image %>">
195
+ <strong><%= item.accepting_user.full_name %></strong> is now your friend!
196
+ </a>
197
+ </p>
198
+ <p>You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!</p>
199
+ <%= render :partial => 'feed_item_footer', :locals => { :item => item } %>
200
+ EOS
201
+
202
+ p {
203
+ text "This is our “new posts in groups you follow” view:"
204
+ }
205
+
206
+ erb 'app/views/feed/items/new_group_posts.html.erb', <<-EOS
207
+ <%= render :partial => 'feed_item_header',
208
+ :locals => { :outer_css_class => 'new_posts',
209
+ :item => item, :header_icon_image => 'new_post.png' } %>
210
+ <h5>New posts in <%= item.new_groups.length %> groups!</h5>
211
+ <%= feed_item_header_closer %>
212
+ <p>There have been new posts in the following groups:
213
+ <ul>
214
+ <% item.new_groups.each do |group| %>
215
+ <li><a href="<%= group_url(group) %>"><strong><%= group.title %></strong> (<%= group.new_posts %> new posts)</a></li>
216
+ <% end %>
217
+ </ul>
218
+ </p>
219
+ <%= render :partial => 'feed_item_footer', :locals => { :item => item } %>
220
+ EOS
221
+
222
+ p {
223
+ text "And this is our “new feature” view:"
224
+ }
225
+
226
+ erb 'app/views/feed/items/new_feature.html.erb', <<-EOS
227
+ <%= render :partial => 'feed_item_header',
228
+ :locals => { :outer_css_class => 'new_feature',
229
+ :item => item, :header_icon_image => 'custom_groups.png' } %>
230
+ <h3>New features have launched!</h3>
231
+ <%= feed_item_header_closer %>
232
+ <h5>We’ve been improving the site for your benefit!</h5>
233
+ <p>You can now create custom groups! We know you’ve all been asking for this feature for a while,
234
+ and it’s now ready for your use! <a href="<%= custom_groups_url %>">Click here</a> to create one!</p>
235
+ <%= render :partial => 'feed_item_footer', :locals => { :item => item } %>
236
+ EOS
237
+
238
+ p {
239
+ text "Is this an improvement? Yes — well, "; em "maybe"; text ". We’ve successfully removed some of the commonality, "
240
+ text "although at the price of introducing a lot of verbosity. We’ve also added some serious weirdness, "
241
+ text "like "; code "div"; text "s that get opened in one partial and closed in another — all of which is, "
242
+ text "of course, added opportunity for error. All in all, it certainly isn’t clean or clear; once again, "
243
+ text "extracting the commonality has come at a serious cost in comprehensibility and readability."
244
+ }
245
+
246
+ p {
247
+ text "Let’s see if using "; code "capture"; text " makes this much better."
248
+ }
249
+
250
+ h5 {
251
+ text "Using "; code "capture"
252
+ }
253
+
254
+ p {
255
+ text "The "; code "capture"; text " method isn’t particularly common or elegant, but it can be useful in "
256
+ text "situations like this to pass HTML into other views. If we use this, let’s see what our shared partial "
257
+ text "looks like first:"
258
+ }
259
+
260
+ erb 'app/views/feed/items/_feed_item_base.html.erb', <<-EOS
261
+ <div class="feed_item feed_item_<%= outer_css_class %>" id="feed_item_<%= item.id %>">
262
+ <div class="feed_item_header">
263
+ <%= image_tag header_icon_image, :class => 'feed_item_image' %>
264
+ <%= header_html %>
265
+ </div>
266
+
267
+ <div class="feed_item_body">
268
+ <%= body_html %>
269
+ </div>
270
+
271
+ <div class="feed_item_footer">Posted at <%= item.created_at %></div>
272
+ </div>
273
+ EOS
274
+
275
+ p {
276
+ text "This certainly looks a lot better than our previous solution of two partials and a helper. It’s simpler, "
277
+ text "and keeps the overall structure of the partial much clearer."
278
+ }
279
+
280
+ p {
281
+ text "Given this, what do our actual feed items look like now?"
282
+ }
283
+
284
+ erb 'app/views/feed/items/accepted_friend_request.html.erb', <<-EOS
285
+ <% header_html = capture do %>
286
+ <h5><strong><%= item.accepting_user.first_name %></strong> accepted your friend request!</h5>
287
+ <% end %>
288
+ <% body_html = capture do %>
289
+ <p>
290
+ <a href="<%= profile_url(item.accepting_user) %>">
291
+ <img src="<%= item.accepting_user.profile_image %>">
292
+ <strong><%= item.accepting_user.full_name %></strong> is now your friend!
293
+ </a>
294
+ </p>
295
+ <p>You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!</p>
296
+ <% end %>
297
+ <%= render :partial => 'feed_item_base', :locals => {
298
+ :outer_css_class => 'accepted_friend_request',
299
+ :item => item,
300
+ :header_icon_image => 'new_friend.png',
301
+ :header_html => header_html,
302
+ :body_html => body_html
303
+ } %>
304
+ EOS
305
+
306
+ erb 'app/views/feed/items/new_group_posts.html.erb', <<-EOS
307
+ <% header_html = capture do %>
308
+ <h5>New posts in <%= item.new_groups.length %> groups!</h5>
309
+ <% end %>
310
+ <% body_html = capture do %>
311
+ <p>There have been new posts in the following groups:
312
+ <ul>
313
+ <% item.new_groups.each do |group| %>
314
+ <li><a href="<%= group_url(group) %>"><strong><%= group.title %></strong> (<%= group.new_posts %> new posts)</a></li>
315
+ <% end %>
316
+ </ul>
317
+ </p>
318
+ <% end %>
319
+ <%= render :partial => 'feed_item_base', :locals => {
320
+ :outer_css_class => 'new_posts',
321
+ :item => item,
322
+ :header_icon_image => 'new_post.png',
323
+ :header_html => header_html,
324
+ :body_html => body_html
325
+ } %>
326
+ EOS
327
+
328
+ erb 'app/views/feed/items/new_feature.html.erb', <<-EOS
329
+ <% header_html = capture do %>
330
+ <h3>New features have launched!</h3>
331
+ <% end %>
332
+ <% body_html = capture do %>
333
+ <h5>We’ve been improving the site for your benefit!</h5>
334
+ <p>You can now create custom groups! We know you’ve all been asking for this feature for a while,
335
+ and it’s now ready for your use! <a href="<%= custom_groups_url %>">Click here</a> to create one!</p>
336
+ <% end %>
337
+ <%= render :partial => 'feed_item_base', :locals => {
338
+ :outer_css_class => 'new_feature',
339
+ :item => item,
340
+ :header_icon_image => 'custom_groups.png',
341
+ :header_html => header_html,
342
+ :body_html => body_html
343
+ } %>
344
+ EOS
345
+
346
+ p {
347
+ text "Alas, although we have a much nicer shared partial this time around, the views have become "
348
+ text "really verbose and quite messy. The use of "; code "capture"; text " clutters up the code and causes a lot of "
349
+ text "action-at-a-distance, and the shared partial takes enough inputs at this point that just calling it "
350
+ text "requires passing many variables that create further visual clutter."
351
+ }
352
+ end
353
+
354
+ def standard_engine_issues
355
+ p {
356
+ text "Hopefully it’s not an overstatement to say, simply: "; em "ugh"; text ". We have a lot of commonality "
357
+ text "in the views we started with that it seems like we "; em "ought"; text " to be able to factor out well, "
358
+ text "and yet both major options we have available don’t do a great job. The result feels messy and inelegant."
359
+ }
360
+
361
+ p {
362
+ text "Let’s dive in deeper and look at some of the specific issues with the results:"
363
+ }
364
+
365
+ ul {
366
+ li {
367
+ strong "Disappearing Structure"; text ": In both refactorings, the overall structure of our views has "
368
+ text "effectively vanished — when using partials and helpers, from the shared code; when using "
369
+ code "capture"; text ", from the callers. This makes the code a lot harder to read, and, more importantly, "
370
+ text "much harder to reason about: it’s all too easy to omit a closing tag, add an extra opening tag, or "
371
+ text "just have a really hard time figuring out how and where to change something. If you wanted to create "
372
+ text "a structure prone to generating lots of bugs, this is almost exactly what you’d do. "
373
+ }
374
+ li {
375
+ strong "Verbosity"; text ": Our refactored code is almost as long as the code it replaces in both cases, "
376
+ text "and a "; em "lot"; text " harder to read. Is this really an improvement?"
377
+ }
378
+ li {
379
+ strong "Lack of Flexibility"; text ": Now consider in both cases what it would take to, for example, be "
380
+ text "able to customize the footer — which is common to all views now, but which you can certainly imagine "
381
+ text "being customized at some point in the near future. With partials and helpers, you suddenly have to "
382
+ text "split the shared code into "; em "another"; text " partial; with "; code "capture"; text ", it’s "
383
+ text "easier, but also involves a gross defaulting of HTML in the shared view and adds more verbosity "
384
+ text "any place it’s customized."
385
+ }
386
+ }
387
+ end
388
+
389
+ def using_fortitude
390
+ p {
391
+ text "OK. So, how can Fortitude help? Because each Fortitude view or partial is simply a Ruby class, we can "
392
+ text "define a base view class here outlining the general structure, and with simple method calls for "
393
+ text "overridden or missing content:"
394
+ }
395
+
396
+ fortitude 'app/views/feed/items/feed_item.html.rb', <<-EOS
397
+ class Views::Feed::Items::FeedItem < Views::Base
398
+ needs :item
399
+
400
+ def content
401
+ div(:class => [ "feed_item", "feed_item_\#{outer_css_class}" ], :id => "feed_item_\#{item.id}") {
402
+ div(:class => 'feed_item_header') {
403
+ image_tag header_icon_image, :class => 'feed_item_image'
404
+ header_content
405
+ }
406
+
407
+ div(:class => 'feed_item_body') { body_content }
408
+
409
+ div("Posted at \#{item.created_at}", :class => 'feed_item_footer')
410
+ }
411
+ end
412
+
413
+ def header_content
414
+ raise "You must implement this method in \#{self.class.name}"
415
+ end
416
+
417
+ def body_content
418
+ raise "You must implement this method in \#{self.class.name}"
419
+ end
420
+
421
+ def outer_css_class
422
+ self.class.name.demodulize.underscore
423
+ end
424
+ end
425
+ EOS
426
+
427
+ p {
428
+ text "Our base class clearly defines the overall structure of all feed item views, and that it "
429
+ code "need"; text "s an "; code "item"; text " in order to be rendered. The "; code "header_content"
430
+ text " and "; code "body_content"; text " methods are left unimplemented, to be provided by subclasses."
431
+ }
432
+
433
+ p {
434
+ text "We’ve also used a neat trick: because this is Ruby code, we’ve used a little bit of metaprogramming "
435
+ text "convention-over-configuration to calculate the "; code "outer_css_class"; text ", instead of having "
436
+ text "to pass it in. This both makes callers cleaner and eliminates a source of inconsistency or error, "
437
+ text "because "; code "outer_css_class"; text " can no longer differ from the name of the view itself "
438
+ text "at all. And yet, exactly because it’s a separate method, if a subclass needed to override this class, "
439
+ text "it could, extremely easily."
440
+ }
441
+
442
+ p {
443
+ text "Given this, what do these subclasses look like? We’ll start with the “accepted friend request” view, "
444
+ text "which turns out to be the most complex one:"
445
+ }
446
+
447
+ fortitude 'app/views/feed/items/accepted_friend_request.html.rb', <<-EOS
448
+ class Views::Feed::Items::AcceptedFriendRequest < FeedItem
449
+ def header_content
450
+ h5 {
451
+ strong item.accepting_user.first_name; text " accepted your friend request!"
452
+ }
453
+ end
454
+
455
+ def body_content
456
+ p {
457
+ a(:href => profile_url(item.accepting_user)) {
458
+ img(:src => item.accepting_user.profile_image)
459
+ strong item.accepting_user.full_name; text " is now your friend!"
460
+ }
461
+ }
462
+ p "You should say hello! It’s only \#{days_until_birthday} days until their birthday!"
463
+ end
464
+
465
+ private
466
+ def days_until_birthday
467
+ (Time.now - item.accepting_user.date_of_birth) / 1.day
468
+ end
469
+ end
470
+ EOS
471
+
472
+ p {
473
+ text "From the view above, it’s hopefully immediately obvious what content "; code "AcceptedFriendRequest"
474
+ text " supplies (its two public methods), and where it goes (since they have clear names). Further, we’ve "
475
+ text "used Fortitude’s ability to add “helper methods” to just a single class to easily factor out the "
476
+ code "days_until_birthday"; text " method."
477
+ }
478
+
479
+ p {
480
+ text "Now, let’s look at the “new group posts” view:"
481
+ }
482
+
483
+ fortitude 'app/views/feed/items/new_group_posts.html.rb', <<-EOS
484
+ class Views::Feed::Items::NewGroupPosts < FeedItem
485
+ def header_content
486
+ h5 "New posts in \#{item.new_groups.length} groups!"
487
+ end
488
+
489
+ def body_content
490
+ p {
491
+ text "There have been new posts in the following groups:"
492
+ ul {
493
+ item.new_groups.each do |group|
494
+ li {
495
+ a(:href => group_url(group)) {
496
+ strong group.title; text " (\#{group.new_posts} new posts)"
497
+ }
498
+ }
499
+ end
500
+ }
501
+ }
502
+ end
503
+ end
504
+ EOS
505
+
506
+ p {
507
+ text "And the “new feature” view:"
508
+ }
509
+
510
+ fortitude 'app/views/feed/items/new_feature.html.rb', <<-EOS
511
+ class Views::Feed::Items::NewFeature < FeedItem
512
+ def header_content
513
+ h3 "New features have launched!"
514
+ end
515
+
516
+ def body_content
517
+ h5 "We’ve been improving the site for your benefit!"
518
+ p {
519
+ text "You can now create custom groups! We know you’ve all been asking for this feature for a while, "
520
+ text "and it’s now ready for your use! "; a("Click here", :href => custom_groups_url); text " to create one!"
521
+ }
522
+ end
523
+ end
524
+ EOS
525
+
526
+ p {
527
+ text "At this point, these views almost seem positively boring — they each contain exactly what you’d "
528
+ text "expect them to contain, with no surprises at all. Given that truly well-factored code often seems "
529
+ text "boring because it’s so straightforward, we’ll take that as a good thing."
530
+ }
531
+ end
532
+
533
+ def fortitude_benefits
534
+ p {
535
+ text "What have we done here? Put simply, we’ve leveraged Ruby’s built-in inheritance mechanism — "
536
+ text "a mechanism every single Ruby programmer on your team already knows extremely well — to build views "
537
+ text "that are far more comprehensible and maintainable than anything we could possibly have achieved "
538
+ text "with traditional templating engines."
539
+ }
540
+
541
+ p {
542
+ text "In many ways, the key behind Fortitude is exactly that it "; em "doesn’t"; text " invent some "
543
+ text "brand-new paradigm for writing your view code. Instead, it allows you to leverage "
544
+ text "all the techniques you already have for factoring the rest of your application, and simply lets you "
545
+ text "apply them to your views."
546
+ }
547
+
548
+ p {
549
+ text "Imagine, for example, that you finally "; em "do"; text " need to customize that shared footer "
550
+ text "in at least one feed-item view. What do you do? Simple: extract it into a method in the base class, "
551
+ text "and override it in whichever view you need to customize it in. You can even easily "
552
+ text "call "; code "super"; text " (or not), either before, after, or in the middle of the overridden "
553
+ text "method, and it behaves exactly how you’d expect, inserting the default footer contents at exactly "
554
+ text "that point in your view."
555
+ }
556
+
557
+ p {
558
+ text "Next, we’ll see how using Fortitude classes can allow us to create a contextual, elegant "
559
+ text "“mini-language” for building complex views very easily."
560
+ }
561
+ end
562
+ end
563
+ end
564
+ end