Introduction

If you like writing object-oriented code, you will probably like the view object pattern. The basic concept is that a PORO (Plain Old Ruby Object) is used to generate HTML for the view. With all the power of object-oriented programming, view objects can make your life a lot easier when it comes to handling complex logic in the view.

Example: Tabs

Consider the example of creating page tabs like below:

Home Page Tabs:

Settings Page Tabs:

Analyzing these two groups of tabs, we see commonalities and differences.

Traits common to all tabs:

Traits unique to each tab group:

In order to create a new group of tabs, a developer should only have to write code about the uniquenesses of those tabs. For example, when creating the settings page tabs, we should only have to specify the links (Profile, My Account), where those links point to, and some logic to determine which link is currently active.

However, in my experience the inclination is to just replicate the totality of the tabs HTML for every implementation of tabs throughout the application, like so:

home.html.erb:

<div class="tabs home-tabs">
  <%= link_to 'Home', root_path, class: "tabs--link #{'is-active' if is_active_home_tab?(:home)}" %>
  <%= link_to 'Dashboard', dashboard_path, class: "tabs--link #{'is-active' if is_active_home_tab?(:dashboard)}" %>
  <% if can?(:manage, @company) %>
    <%= link_to 'Company Admin', company_path(@company), class: "tabs--link #{'is-active' if is_active_home_tab?(:company)}" %>
  <% end %>
</div>

settings.html.erb:

<div class="tabs settings-tabs">
  <%= link_to 'Profile', profile_path(current_user.profile), class: "tabs--link #{'is-active' if is_active_settings_tab?(:profile)}" %>
  <%= link_to 'My Account', edit_user_registration_path(current_user), class: "tabs--link #{'is-active' if is_active_settings_tab?(:user}" %>
</div>

application_helper.rb

def is_active_home_tab?(tab)
  case tab
  when :home
    return true if controller_name == 'home'
  when :dashboard
    return true if controller_name == 'dashboards'
  when :company
    return true if controller_name == 'companies'
  end
  return false
end

def is_active_settings_tab?(tab)
  case tab
  when :profile
    return true if controller_name == 'profiles'
  when :user
    return true if controller_name == 'users'
  end      
  return false
end

There are quite a few problems with this approach. First of all, the HTML structure of our tabs component is duplicated in multiple spots. For example, they must be wrapped in a div with a class tabs and each tab within the group must have the class tabs–link. The knowledge of that structure is in both home.html.erb and settings.html.erb. If we need to modify the structure of the tabs, either to change the styling or behavior, we’ll need to do it in both of these files. This is the very reason why upgrading a framework like Bootstrap is such a pain – there are dependencies to the HTML structure and CSS classes all over the place.

Secondly, the logic about which tabs a user can see based on their permissions is just written with the ERB if statement. While in this example it is not a big deal, if the logic to determine whether or not to show particular tabs was more complex, performing it in the view would get unwieldy.

Finally, the implementation of the “is-active” class is clunky at best, and isn’t really something we want implemented in the application helper since it doesn’t really pertain to every part of the site.

Implementing View Objects

Boilerplate Code

While view objects can be implemented however you choose – they are just plain old ruby classes, afterall – there is boilerplate functionality that every view object should have. Some examples include:- Route helpers- Helper methods like numbertocurrency- CanCanCan methods (if you use the gem)- The view context (for methods like controller_name)

Because of that, I’m going to start by offering some boilerplate code that you can add to your Rails project to be able to start efficiently using view objects.

Let’s start with our base ViewObject class that all view objects inherit from:

# app/view_objects/view_object.rb

class ViewObject
  attr_reader :context

  include Rails.application.routes.url_helpers
  include ActionView::Helpers
  include ActionView::Context

  # comment these two lines out if not using cancan
  include CanCan::ControllerAdditions
  delegate :current_ability, :to => :context

  def initialize(context, args = {})
    @context = context
    after_init(args)
  end

  def after_init(args = {})
    # implemented by child classes
  end
end

Now we need to autoload the classes in app/view_objects/

# application.rb

config.autoload_paths += Dir["#{config.root}/app/view_objects/**/"]

Note: make sure to restart your server after changing application.rb

Last but not least, let’s add this helper method to application_helper:

# application_helper.rb

def view_object(name, args = {})
  if name.is_a?(Symbol)
    class_name = name.to_s.titleize.split(" ").join("")
  else
    class_name = name.split("/").map {|n| n.titleize.sub(" ", "") }.join("::")
  end
  class_name.constantize.new(self, args)
end

This view_object helper method dynamically finds and instantiates the correct view object class based on the name provided. This way, we can call <%= view_object(:home_tabs, company: @company).html %> from our view to instantiate a view object class called HomeTabs and automatically pass in the view context.

Creating our first View Object

The basic idea is that we can just create a plain old ruby class that ultimately spits out some HTML. Let’s start by creating the HomeTabs view object:

app/viewobjects/hometabs.rb:

class HomeTabs < ViewObject
  attr_reader :company

  def html
    content_tag :div, tabs.join('').html_safe, class: 'tabs'
  end

  private

    def after_init(args = {})
      @company = args[:company]
    end

    def tabs
      [home_tab, dashboard_tab, company_tab].compact
    end

    def home_tab
      build_tab('Home', home_path, :home)
    end

    def dashboard_tab
      build_tab('Dashboard', dashboard_path, :dashboard)
    end

    def company_tab
      return nil unless can?(:manage, company)
      build_tab('Company Admin', company_path, :company)
    end

    def build_tab(text, path, tab_name)
      link_to text, path, class: tab_class(tab_name)
    end

    def tab_class(tab)
      active_class = active?(tab) ? 'is-active' : nil
      ['tabs--link', active_class].compact.join(' ')
    end

    def active?(tab)
      case tab
      when :home
        return true if context.controller_name == 'home'
      when :dashboard
        return true if context.controller_name == 'dashboards'
      when :company
        return true if context.controller_name == 'companies'
      end
      return false
    end
end

Now in the view (home.html.erb):

<%= view_object(:home_tabs, company: @company).html %>

While this is quite a bit of code just to implement four tabs, the real value is realized when we want to make our next group of tabs. First, let’s try to figure out which methods in the above HomeTabs class are specific to the tabs rendered on the home page versus generic to all tabs throughout the app.

Methods specific to HomeTabs view object:- hometab- dashboardtab- company_tab

Methods generic to all tabs view objects:- html- tabclass- buildtab

Interface methods (i.e. every tabs view object must implement but implementation changes):- active?- tabs

Now, let’s refactor this into two classes (1) a generic “Tabs” class, and (2) the “HomeTabs” class

app/view_objects/tabs.rb:

class Tabs < ViewObject
  def html
    content_tag :div, tabs.join('').html_safe, class: 'tabs'
  end

  private

    # Interface Methods

    def tabs
      fail 'must be implemented by subclass'
    end

    def active?(tab)
      fail 'must be implemented by subclass'
    end

    # Generic Methods

    def tab_class(tab)
      active_class = active?(tab) ? 'is-active' : nil
      ['tabs--link', active_class].compact.join(' ')
    end

    def build_tab(text, path, tab)
      link_to text, path, class: tab_class(tab)
    end
end

app/viewobjects/hometabs.rb:

class HomeTabs < Tabs
  private

    # Interface Methods

    def tabs
      [home_tab, dashboard_tab, company_tab].compact
    end

    def active?(tab)
      case tab
      when :home
        return true if context.controller_name == 'home'
      when :dashboard
        return true if context.controller_name == 'dashboards'
      when :company
        return true if context.controller_name == 'companies'
      end
      return false
    end

    # Specific Methods

    def home_tab
      build_tab('Home', home_path, :home)
    end

    def dashboard_tab
      build_tab('Dashboard', dashboard_path, :dashboard)
    end

    def company_tab
      return nil unless can?(:manage, company)
      build_tab('Company Admin', company_path, :company)
    end
end

What I hope you’ve noticed is that the code in the HomeTabs class has been cut down to the core essence of what it means to be HomeTabs instead of any other type of tabs. That is, only code specific to HomeTabs is here – not code generic to all tabs. For example, CSS classes are not set within this HomeTabs view object, since the tabs’ CSS classes are the same for all tabs.

View Objects are Scalable

Now here’s where the beauty of View Objects really kicks in. Let’s create a new set of tabs: the settings tabs.

app/viewobjects/settingstabs.rb:

class SettingsTabs < Tabs
  private

    # Interface Methods

    def tabs
      [profile_tab, user_tab].compact
    end

    def active?(tab)
      case tab
      when :profile
        return true if context.controller_name == 'profiles'
      when :user
        return true if context.controller_name == 'users'
      end      
      return false
    end

    # Specific Methods

    def profile_tab
      build_tab('Profile', profile_path(context.current_user.profile), :profile)
    end

    def user_tab
      build_tab('My Account', edit_user_registration_path(current_user), :user)
    end
end

And then in the view (settings.html.erb):

<%= view_object(:settings_tabs).html %>

We need only follow the pattern already laid out in HomeTabs to implement a brand new set of fully functioning tabs. If we want to modify the way our tabs are structured, we can do so in one place: the generic “Tabs” view object. Further, should the logic become more complex surrounding what tabs to show under different circumstances, we have the power of object-oriented code to help ease the pain.

Ryan Francis

Partner & Developer

As ambitious as he is tall, Ryan has a passion for creation. In 2012, he created Francis Lofts & Bunks, a company in Western Ohio that manufactures aluminum loft beds and bunk beds. Equipped with a burning desire to build things that are useful to others, Ryan has come into his own in web development, combining creativity, logic, and an empathy for others to build outstanding, easy-to-use products.

Ready to Build Something Great?

Partner with us to develop technology to grow your business.