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