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.
- 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 +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
|