fortitude 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +28 -22
- data/CHANGES.md +50 -0
- data/CONTRIBUTORS.md +1 -0
- data/doc/.gitignore +18 -0
- data/doc/Gemfile +21 -0
- data/doc/config.rb +92 -0
- data/doc/source/images/background.png +0 -0
- data/doc/source/images/middleman.png +0 -0
- data/doc/source/images/why/icon_button.png +0 -0
- data/doc/source/images/why/icon_button@2x.png +0 -0
- data/doc/source/images/why/modal_dialog@2x.png +0 -0
- data/doc/source/index.html.pcss +96 -0
- data/doc/source/index.html.rb +66 -0
- data/doc/source/javascripts/all.js +1 -0
- data/doc/source/javascripts/highlight.pack.js +1 -0
- data/doc/source/layouts/layout.rb +55 -0
- data/doc/source/portable/fortitude-bootstrap.rb +53 -0
- data/doc/source/shared/base.pcss +62 -0
- data/doc/source/shared/base.rb +30 -0
- data/doc/source/shared/common.rb +55 -0
- data/doc/source/shared/standard_page.rb +40 -0
- data/doc/source/stylesheets/_shared_prefix.scss +25 -0
- data/doc/source/stylesheets/all.css.scss +7 -0
- data/doc/source/stylesheets/basics.css.scss +20 -0
- data/doc/source/stylesheets/bootstrap_importer.css.scss +1 -0
- data/doc/source/stylesheets/highlight/arta.css +140 -0
- data/doc/source/stylesheets/highlight/ascetic.css +52 -0
- data/doc/source/stylesheets/highlight/atelier-dune.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-dune.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-forest.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-forest.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-heath.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-heath.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-lakeside.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-lakeside.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-seaside.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-seaside.light.css +95 -0
- data/doc/source/stylesheets/highlight/brown_paper.css +104 -0
- data/doc/source/stylesheets/highlight/brown_papersq.png +0 -0
- data/doc/source/stylesheets/highlight/codepen-embed.css +108 -0
- data/doc/source/stylesheets/highlight/color-brewer.css +168 -0
- data/doc/source/stylesheets/highlight/dark.css +104 -0
- data/doc/source/stylesheets/highlight/default.css +152 -0
- data/doc/source/stylesheets/highlight/docco.css +135 -0
- data/doc/source/stylesheets/highlight/far.css +111 -0
- data/doc/source/stylesheets/highlight/foundation.css +136 -0
- data/doc/source/stylesheets/highlight/github.css +124 -0
- data/doc/source/stylesheets/highlight/googlecode.css +147 -0
- data/doc/source/stylesheets/highlight/hybrid.css +170 -0
- data/doc/source/stylesheets/highlight/idea.css +125 -0
- data/doc/source/stylesheets/highlight/ir_black.css +109 -0
- data/doc/source/stylesheets/highlight/kimbie.dark.css +96 -0
- data/doc/source/stylesheets/highlight/kimbie.light.css +96 -0
- data/doc/source/stylesheets/highlight/magula.css +121 -0
- data/doc/source/stylesheets/highlight/mono-blue.css +69 -0
- data/doc/source/stylesheets/highlight/monokai.css +127 -0
- data/doc/source/stylesheets/highlight/monokai_sublime.css +154 -0
- data/doc/source/stylesheets/highlight/obsidian.css +153 -0
- data/doc/source/stylesheets/highlight/paraiso.dark.css +95 -0
- data/doc/source/stylesheets/highlight/paraiso.light.css +95 -0
- data/doc/source/stylesheets/highlight/pojoaque.css +107 -0
- data/doc/source/stylesheets/highlight/pojoaque.jpg +0 -0
- data/doc/source/stylesheets/highlight/railscasts.css +187 -0
- data/doc/source/stylesheets/highlight/rainbow.css +108 -0
- data/doc/source/stylesheets/highlight/school_book.css +112 -0
- data/doc/source/stylesheets/highlight/school_book.png +0 -0
- data/doc/source/stylesheets/highlight/solarized_dark.css +108 -0
- data/doc/source/stylesheets/highlight/solarized_light.css +108 -0
- data/doc/source/stylesheets/highlight/sunburst.css +164 -0
- data/doc/source/stylesheets/highlight/tomorrow-night-blue.css +95 -0
- data/doc/source/stylesheets/highlight/tomorrow-night-bright.css +94 -0
- data/doc/source/stylesheets/highlight/tomorrow-night-eighties.css +94 -0
- data/doc/source/stylesheets/highlight/tomorrow-night.css +95 -0
- data/doc/source/stylesheets/highlight/tomorrow.css +92 -0
- data/doc/source/stylesheets/highlight/vs.css +93 -0
- data/doc/source/stylesheets/highlight/xcode.css +158 -0
- data/doc/source/stylesheets/highlight/zenburn.css +118 -0
- data/doc/source/why/a_larger_view.html.rb +774 -0
- data/doc/source/why/a_simple_helper.html.rb +332 -0
- data/doc/source/why/building_a_rich_modal_dialog.html.rb +156 -0
- data/doc/source/why/commonality_and_inheritance.html.rb +564 -0
- data/doc/source/why/example_list.rb +60 -0
- data/doc/source/why/example_page.rb +116 -0
- data/doc/source/why/index.html.rb +86 -0
- data/doc/source/why/other_benefits.html.rb +110 -0
- data/fortitude.gemspec +6 -1
- data/lib/fortitude/doctypes/html4_tags_strict.rb +1 -0
- data/lib/fortitude/doctypes/html5.rb +1 -0
- data/lib/fortitude/method_templates/tag_method_template.rb.smpl +27 -14
- data/lib/fortitude/rails/helpers.rb +2 -2
- data/lib/fortitude/rendering_context.rb +10 -1
- data/lib/fortitude/tags/tag.rb +3 -2
- data/lib/fortitude/tags/tag_support.rb +8 -3
- data/lib/fortitude/tilt/fortitude_template.rb +6 -2
- data/lib/fortitude/version.rb +1 -1
- data/lib/fortitude/widget.rb +2 -0
- data/lib/fortitude/widget/convenience.rb +30 -0
- data/lib/fortitude/widget/files.rb +22 -11
- data/lib/fortitude/widget/needs.rb +5 -3
- data/spec/helpers/system_helpers.rb +1 -0
- data/spec/rails/development_mode_system_spec.rb +0 -1
- data/spec/rails/rendering_system_spec.rb +20 -1
- data/spec/rails/templates/rendering_system_spec/app/controllers/rendering_system_spec_controller.rb +13 -0
- data/spec/rails/templates/rendering_system_spec/app/views/rendering_system_spec/render_hash_subclass.rb +18 -0
- data/spec/rails/templates/rendering_system_spec/lib/my_hash.rb +5 -0
- data/spec/system/convenience_methods_system_spec.rb +166 -0
- data/spec/system/formatting_system_spec.rb +25 -1
- data/spec/system/tag_rendering_system_spec.rb +73 -0
- data/spec/system/tilt_system_spec.rb +31 -0
- data/spec/system/widget_class_from_spec.rb +20 -4
- metadata +92 -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
|