require 'source/why/example_page' module Views module Why class CommonalityAndInheritance < Views::Why::ExamplePage def example_intro p { text "In our last example, we saw how Fortitude allows you to share code among methods in the same " text "view and how it lets you pass blocks to allow very flexible reuse and customization of that code." } p { text "Here, we’ll explore how Fortitude allows you to use Ruby’s inheritance mechanism to easily factor " text "out commonality among several related views. Using inheritance is one of the most powerful " text "features of Fortitude." } end def example_description p { text "Imagine we’re building the initial phases of a social network, and the time has come to create our " text "main social feed. This feed is going to list various events: friends who have accepted our friend " text "request, new posts in groups we follow, and even new features we’ve introduced to the site." } p { text "This presents an interesting challenge for building views. The views for these feed items are likely " text "to have quite a bit in common — yet almost all of it is going to be customized at "; em "some" text " point. Still, let’s start simply, with just the three feed items mentioned above." } p { text "First, the “accepted friend request” view:" } erb 'app/views/feed/items/accepted_friend_request.html.erb', <<-EOS
<%= item.accepting_user.full_name %> is now your friend!
You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!
There have been new posts in the following groups:
You can now create custom groups! We know you’ve all been asking for this feature for a while, and it’s now ready for your use! Click here to create one!
<%= item.accepting_user.full_name %> is now your friend!
You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!
<%= render :partial => 'feed_item_footer', :locals => { :item => item } %> EOS p { text "This is our “new posts in groups you follow” view:" } erb 'app/views/feed/items/new_group_posts.html.erb', <<-EOS <%= render :partial => 'feed_item_header', :locals => { :outer_css_class => 'new_posts', :item => item, :header_icon_image => 'new_post.png' } %>There have been new posts in the following groups:
You can now create custom groups! We know you’ve all been asking for this feature for a while, and it’s now ready for your use! Click here to create one!
<%= render :partial => 'feed_item_footer', :locals => { :item => item } %> EOS p { text "Is this an improvement? Yes — well, "; em "maybe"; text ". We’ve successfully removed some of the commonality, " text "although at the price of introducing a lot of verbosity. We’ve also added some serious weirdness, " text "like "; code "div"; text "s that get opened in one partial and closed in another — all of which is, " text "of course, added opportunity for error. All in all, it certainly isn’t clean or clear; once again, " text "extracting the commonality has come at a serious cost in comprehensibility and readability." } p { text "Let’s see if using "; code "capture"; text " makes this much better." } h5 { text "Using "; code "capture" } p { text "The "; code "capture"; text " method isn’t particularly common or elegant, but it can be useful in " text "situations like this to pass HTML into other views. If we use this, let’s see what our shared partial " text "looks like first:" } erb 'app/views/feed/items/_feed_item_base.html.erb', <<-EOS
<%= item.accepting_user.full_name %> is now your friend!
You should say hello! It’s only <%= ((Time.now - item.accepting_user.date_of_birth) / 1.day) %> days until their birthday!
<% end %> <%= render :partial => 'feed_item_base', :locals => { :outer_css_class => 'accepted_friend_request', :item => item, :header_icon_image => 'new_friend.png', :header_html => header_html, :body_html => body_html } %> EOS erb 'app/views/feed/items/new_group_posts.html.erb', <<-EOS <% header_html = capture do %>There have been new posts in the following groups:
You can now create custom groups! We know you’ve all been asking for this feature for a while, and it’s now ready for your use! Click here to create one!
<% end %> <%= render :partial => 'feed_item_base', :locals => { :outer_css_class => 'new_feature', :item => item, :header_icon_image => 'custom_groups.png', :header_html => header_html, :body_html => body_html } %> EOS p { text "Alas, although we have a much nicer shared partial this time around, the views have become " text "really verbose and quite messy. The use of "; code "capture"; text " clutters up the code and causes a lot of " text "action-at-a-distance, and the shared partial takes enough inputs at this point that just calling it " text "requires passing many variables that create further visual clutter." } end def standard_engine_issues p { text "Hopefully it’s not an overstatement to say, simply: "; em "ugh"; text ". We have a lot of commonality " text "in the views we started with that it seems like we "; em "ought"; text " to be able to factor out well, " text "and yet both major options we have available don’t do a great job. The result feels messy and inelegant." } p { text "Let’s dive in deeper and look at some of the specific issues with the results:" } ul { li { strong "Disappearing Structure"; text ": In both refactorings, the overall structure of our views has " text "effectively vanished — when using partials and helpers, from the shared code; when using " code "capture"; text ", from the callers. This makes the code a lot harder to read, and, more importantly, " text "much harder to reason about: it’s all too easy to omit a closing tag, add an extra opening tag, or " text "just have a really hard time figuring out how and where to change something. If you wanted to create " text "a structure prone to generating lots of bugs, this is almost exactly what you’d do. " } li { strong "Verbosity"; text ": Our refactored code is almost as long as the code it replaces in both cases, " text "and a "; em "lot"; text " harder to read. Is this really an improvement?" } li { strong "Lack of Flexibility"; text ": Now consider in both cases what it would take to, for example, be " text "able to customize the footer — which is common to all views now, but which you can certainly imagine " text "being customized at some point in the near future. With partials and helpers, you suddenly have to " text "split the shared code into "; em "another"; text " partial; with "; code "capture"; text ", it’s " text "easier, but also involves a gross defaulting of HTML in the shared view and adds more verbosity " text "any place it’s customized." } } end def using_fortitude p { text "OK. So, how can Fortitude help? Because each Fortitude view or partial is simply a Ruby class, we can " text "define a base view class here outlining the general structure, and with simple method calls for " text "overridden or missing content:" } fortitude 'app/views/feed/items/feed_item.html.rb', <<-EOS class Views::Feed::Items::FeedItem < Views::Base needs :item def content div(:class => [ "feed_item", "feed_item_\#{outer_css_class}" ], :id => "feed_item_\#{item.id}") { div(:class => 'feed_item_header') { image_tag header_icon_image, :class => 'feed_item_image' header_content } div(:class => 'feed_item_body') { body_content } div("Posted at \#{item.created_at}", :class => 'feed_item_footer') } end def header_content raise "You must implement this method in \#{self.class.name}" end def body_content raise "You must implement this method in \#{self.class.name}" end def outer_css_class self.class.name.demodulize.underscore end end EOS p { text "Our base class clearly defines the overall structure of all feed item views, and that it " code "need"; text "s an "; code "item"; text " in order to be rendered. The "; code "header_content" text " and "; code "body_content"; text " methods are left unimplemented, to be provided by subclasses." } p { text "We’ve also used a neat trick: because this is Ruby code, we’ve used a little bit of metaprogramming " text "convention-over-configuration to calculate the "; code "outer_css_class"; text ", instead of having " text "to pass it in. This both makes callers cleaner and eliminates a source of inconsistency or error, " text "because "; code "outer_css_class"; text " can no longer differ from the name of the view itself " text "at all. And yet, exactly because it’s a separate method, if a subclass needed to override this class, " text "it could, extremely easily." } p { text "Given this, what do these subclasses look like? We’ll start with the “accepted friend request” view, " text "which turns out to be the most complex one:" } fortitude 'app/views/feed/items/accepted_friend_request.html.rb', <<-EOS class Views::Feed::Items::AcceptedFriendRequest < FeedItem def header_content h5 { strong item.accepting_user.first_name; text " accepted your friend request!" } end def body_content p { a(:href => profile_url(item.accepting_user)) { img(:src => item.accepting_user.profile_image) strong item.accepting_user.full_name; text " is now your friend!" } } p "You should say hello! It’s only \#{days_until_birthday} days until their birthday!" end private def days_until_birthday (Time.now - item.accepting_user.date_of_birth) / 1.day end end EOS p { text "From the view above, it’s hopefully immediately obvious what content "; code "AcceptedFriendRequest" text " supplies (its two public methods), and where it goes (since they have clear names). Further, we’ve " text "used Fortitude’s ability to add “helper methods” to just a single class to easily factor out the " code "days_until_birthday"; text " method." } p { text "Now, let’s look at the “new group posts” view:" } fortitude 'app/views/feed/items/new_group_posts.html.rb', <<-EOS class Views::Feed::Items::NewGroupPosts < FeedItem def header_content h5 "New posts in \#{item.new_groups.length} groups!" end def body_content p { text "There have been new posts in the following groups:" ul { item.new_groups.each do |group| li { a(:href => group_url(group)) { strong group.title; text " (\#{group.new_posts} new posts)" } } end } } end end EOS p { text "And the “new feature” view:" } fortitude 'app/views/feed/items/new_feature.html.rb', <<-EOS class Views::Feed::Items::NewFeature < FeedItem def header_content h3 "New features have launched!" end def body_content h5 "We’ve been improving the site for your benefit!" p { text "You can now create custom groups! We know you’ve all been asking for this feature for a while, " text "and it’s now ready for your use! "; a("Click here", :href => custom_groups_url); text " to create one!" } end end EOS p { text "At this point, these views almost seem positively boring — they each contain exactly what you’d " text "expect them to contain, with no surprises at all. Given that truly well-factored code often seems " text "boring because it’s so straightforward, we’ll take that as a good thing." } end def fortitude_benefits p { text "What have we done here? Put simply, we’ve leveraged Ruby’s built-in inheritance mechanism — " text "a mechanism every single Ruby programmer on your team already knows extremely well — to build views " text "that are far more comprehensible and maintainable than anything we could possibly have achieved " text "with traditional templating engines." } p { text "In many ways, the key behind Fortitude is exactly that it "; em "doesn’t"; text " invent some " text "brand-new paradigm for writing your view code. Instead, it allows you to leverage " text "all the techniques you already have for factoring the rest of your application, and simply lets you " text "apply them to your views." } p { text "Imagine, for example, that you finally "; em "do"; text " need to customize that shared footer " text "in at least one feed-item view. What do you do? Simple: extract it into a method in the base class, " text "and override it in whichever view you need to customize it in. You can even easily " text "call "; code "super"; text " (or not), either before, after, or in the middle of the overridden " text "method, and it behaves exactly how you’d expect, inserting the default footer contents at exactly " text "that point in your view." } p { text "Next, we’ll see how using Fortitude classes can allow us to create a contextual, elegant " text "“mini-language” for building complex views very easily." } end end end end