In Progress
Unit 1, Lesson 21
In Progress

Presenter and View

Join sous-chef Federico Iachetti to dive a little deeper into the Presenter Pattern for organizing web app views! Today you’ll see how giving presenters access to the View object opens up a whole new level of possibilities for cleaning up your view templates. Enjoy!

Video transcript & code

In Episode #585 we talked about using presenters as intermediate objects for display logic that could either go on the model or the view, but didn't belong to either.

We ended up with a view that instantiated a UserPresenter giving it a user and the view itself,


view = View.new( user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(user, self) %>

    Profile of <%= user_presenter %>

    Born on <%= user_presenter.birth_date %>
  ERB
end

But when we review our presenter, we discover that the view object was never used.


class UserPresenter
  def initialize(user, view)
    @user = user
    @view = view
  end

  def to_s
    "#{@user.last_name}, #{@user.first_name}"
  end

  def birth_date
    @user.birth_date.strftime("%B %-d %Y")
  end
end

Let's see why it's important for us to have a reference to the view.

Say we need to extend our view by displaying the user's current account balance.

To accomplish this we can use a regular old helper method in the view and it'd be fine.


view = View.new( user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(user, self) %>

    Profile of <%= user_presenter %>

    Born on <%= user_presenter.birth_date %>

    Balance: <%= number_to_currency(user_presenter.balance) %>
  ERB
end

But now we want to show it in red if the balance is negative and green if it's positive. So we can move the logic to the presenter


class UserPresenter
  # ...

  def balance
    color = @user.balance >= 0 ? 'green' : 'red'

    "[#{color}] #{@view.number_to_currency(@user.balance)} [/#{color}]"
  end
end

And use the helper right there.


view = View.new( user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(user, self) %>

    Profile of <%= user_presenter %>

    Born on <%= user_presenter.birth_date %>

    Balance: <%= user_presenter.balance %>
  ERB
end

Now the view is cleaner and the logic is hidden behind the presenter.

And since we delegated all the minute details to the presenter object the view code is also more consistent

And it renders as expected


puts view.render

# >> Profile of Iachetti, Federico
# >>
# >> Born on 1981-12-06
# >>
# >> Balance: [green] $45.00 [/green]

Now, in every web app we have the need to link to things.

Say we're coding a top bar partial for our site (yes, we'll use HTML this time around).

In modern web frameworks we don't just hardcode links


view = View.new( current_user: user ) do
  <<~ERB
      <% user_presenter = UserPresenter.new(current_user, self) %>
  <nav class="right"%>
 ERB 
end

We use routing helpers to make sure we're linking to the appropriate resource. And routing helpers are present on the views.

This is true at least in a Rails application.


view = View.new( current_user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(current_user, self) %>

    <%nav class="right">
       <%= link_to current_user.name, user_path(current_user) %>
    </nav>
  ERB
end

Is this logic worth a method on our presenter? Debatable… but probably not.

On the other hand, if we plan to use this link elsewhere in the application, having the presenter object as the single source of truth doesn't sound bad at all.

But now, if we decide that we want to show a link to log in or sign up if the user hasn't authenticated, our view starts to gain complexity


view = View.new( current_user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(current_user, self) %>

    <%nav class="right">
      <% if current_user %>
        <%= link_to current_user.name, user_path(current_user) %>
      <% else %>
        <%= link_to "Log in", sign_in_path %>
        or 
        <%= link_to "Sign up", sign_up_path %>
      <% end %>
    </nav>
  ERB
end

Now it deserves to be moved into the presenter.

We start by writing the code we want to have in the view.


view = View.new( current_user: user ) do
  <<~ERB
    <% user_presenter = UserPresenter.new(current_user, self) %>

    <%nav class="right">
      <%= user_presenter.profile_link %>
    </nav>
  ERB
end

And we paste the code we extracted to the presenter removing the ERB tag markers.


class UserPresenter
  # ...

  def profile_link
    if current_user
      link_to user_presenter, user_path(current_user)
    else
      link_to "Log in", sign_in_path
      or
      link_to "Sign up", sign_up_path
    end
  end
end

Now we change all the instances of current_user to the user getter.

Then we remove all the instances of user_presenter. There are none in this case.

And we delegate all the helpers to the view.


class UserPresenter
  # ...

  def profile_link
    if view.current_user
      view.link_to name, view.user_path(user)
    else
      view.link_to("Log in",  view.sign_in_path) +
        ' or ' +
        view.link_to("Sign up", view.sign_up_path)
    end
  end
end

Finally, we can render our view with a current user


view = View.new( current_user: User.new(first_name: 'Avdi', last_name: 'Grimm') ) do
  <<~ERB <% user_presenter = UserPresenter.new(current_user, self) %> <%nav class="right"> <%= user_presenter.profile_link %> </nav> ERB end puts view.render # >>
# >> <%nav class="right">
# >>   Federico
# >> </nav>

And if we set the current user to nil we get our authentication links.


view = View.new( current_user: nil ) do
  <<~ERB <% user_presenter = UserPresenter.new(current_user, self) %> <%nav class="right"> <%= user_presenter.profile_link %> </nav> ERB end puts view.render # >> 
# >> <%nav class="right">
# >>   Federico
# >> </nav>

In conclusion, presenter objects give us the possibility to clean up views without touching our models.

And having the view available in the context of the presenter allows us to make our presenters very versatile by using the tools that our favorite frameworks already give us out of the box.

Happy Hacking!

Responses