Here's a quick tip about how I used my presenters to handle a collection of objects.
Previous posts relevant to this are:
- Ruby delegate.rb Secrets
- The easiest way to handle displaying bad data
- Simplify your code with your own conventions
Working with collections of objects
A common problem when displaying data comes when we display collections of objects that contain collections of other objects.
I solved this for myself in a simple way with my existing presenters.
My application needed to display search results from an event management system and we called this the Agenda. Our agenda had sessions, days, and time slots and we needed a way to handle presentation details for all of them.
All of these items needed their own code, but we only needed to work with them together. From the start, we didn't break these presenters out into separate files.
Here's the structure of how we managed our agenda presenters
module AgendaBuilder
class ResultsPresenter < ::Presenter
end
class DayPresenter < ::Presenter
end
class TimeSlotPresenter < ::Presenter
end
class SessionPresenter < ::Presenter
end
end
This kept our related details together and kept us focused in one place.
Custom iterators
When in came to using these in our view templates, we didn't want to leak knowledge of the object classes into the views. From the start, a simple approach would be to initialize the presenters where you need them. Here's what it could have looked like in our ERB files:
<% agenda.sessions.each do |session| %>
<% session_presenter = AgendaBuilder::SessionPresenter.new(session, self)
%>
-
<%= session_presenter.title %>
<%= session_presenter.other_view_method %>
<% end %>
This is ugly. The view template has knowlegde of the classes used to implement the objects it needs to display. Instead, it would be far easier to read and handle changes if it looked like this:
<% agenda.each_session do |session| %>
-
<%= session.title %>
<%= session.other_view_method %>
<% end %>
Now that is far easier to grok.
Let's take a look at the code:
class ResultsPresenter < ::Presenter
def each_session(&block)
presenter = AgendaBuilder::SessionPresenter.new(nil, view)
sessions.each do |session|
presenter.session = session
block.call(presenter)
end
end
end
This creates the each_session
method which accepts a block that we use for each session in the collection.
The first part of this method may look strange: we initialize a SessionPresenter
wrapping nil
and providing the view
object.
The presenter requires some object to initialize properly and since we're setting the session object later, we can just use nil as a placeholder. But we do this so that we can avoid creating a new presenter with each iteration of the block.
The alternative would look like this:
class ResultsPresenter < ::Presenter
def each_session(&block)
sessions.each do |session|
presenter = AgendaBuilder::SessionPresenter.new(session, view)
block.call(presenter)
end
end
end
While this would work, there's no need to create a new presenter object each time. Our handy session=
method does the trick.
Benefits of custom iterators
Providing our own iteration method gave us the ability to change the behavior as we needed. If we merely rely on an Array with agenda.sessions.each
, we're tied to that dependecy.
If we decide we don't need a SessionPresenter
at all, we don't need to change our view code, we'd only need to remove that from our each_session
method.
Following the pattern
We had this need for custom iterators in several places (sessions, days, and time slots) so we have an established pattern. The next step was to move this to our Presenter
class so we didn't have to rewrite the same procedure each time.
The only differences between our iterators were the collection of objects (sessions, days, and time slots) and the class of the presenters needed.
All we really want to write is something like this:
class ResultsPresenter < ::Presenter
def each_session(&block)
wrapped_enum(AgendaBuilder::SessionPresenter, sessions, &block)
end
end
With some minor changes to our procedure, we end up with a base wrapped_enum
like this:
class Presenter < SimpleDelegator
def wrapped_enum(presenter_class, enumerable, &block)
presenter = presenter_class.new(nil, view)
enumerable.each do |object|
presenter.__setobj__(object)
block.call(presenter)
end
end
end
We went back to our SimpleDelegator __setobj__
method to avoid knowledge about the domain of the presenter.
Iterating over and presenting items from our collections became so much easier and our views so much simpler. This allowed us to treat our views more like readable configuration. A title method called in the view template could be the actual title from our wrapped object just passing the data through, or, as we change our requirements, could become anything else without requiring changes to our template.
Our view template captured our intent, whereas our presenter captured the requirements and implementation.