tombeynon
tombeynon

Reputation: 2346

Recommended way to use Rails view helpers in a presentation class

I've been researching the 'recommended' way to use Rails view helpers (e.g. link_to, content_tag) in a plain ruby class, such as a presenter. It seems there's very little information on this front and I wanted to get an idea of what the Stack community thought.

So, the options we have are.. (note I'm using Rails 4, and am less concerned about older versions)

Include the required modules manually

This is probably the cleanest way, since only the helpers needed are included. However I have found this method to not work in some cases, as the usual view context provided in plain Rails helpers is configured for the current request. url_for wouldn't know about the current request for example, so the host might not match.

class MyPresenter
    include ActionView::Helpers::UrlHelper
    include ActionView::Helpers::CaptureHelper

    def wrapped_link
        content_tag :div, link_to('My link', root_url)
    end
end

Use ActionController::Base.helpers

Since Rails 3, ActionController::Base has included a helpers method to access the current view context. I believe the view context provided by this method is configured as it would be in a rails helper, but I might be wrong. There's not really any documentation about this which seems worrying, but it does work quite well in practice.

class MyPresenter
    def wrapped_link
        h.content_tag :div, h.link_to('My link', h.root_url)
    end

    protected

    def h
        ActionController::Base.helpers
    end
end

I believe this view context can also be mixed in with include, but the rails view helpers have hundreds of methods and it feels dirty to include them all indiscriminately.

Inject the view context when calling the presenter

Finally, we could just pass the view context to the class when it's initialized (or alternatively in a render method)

class MyPresenter
    attr_accessor :context
    alias_method :h, :context

    def initialize(context)
        @context = context
    end

    def wrapped_link
        h.content_tag :div, h.link_to('My link', h.root_url)
    end
end

class MyController < ApplicationController
    def show
        # WARNING - `view_context` in a controller creates an object
        @presenter = MyPresenter.new(view_context) 
    end
end

Personally I tend to lean towards the latter two options, but with no definitive answer from the Rails team (that I've been able to find) I felt a bit unsure. Who better to ask than Stack!

Upvotes: 28

Views: 6404

Answers (2)

fltiago
fltiago

Reputation: 178

I would go with the mix of the second and third option, something like:

class MyPresenter
  def initialize(helpers)
    @h = helpers
  end

  def wrapped_link
    h.content_tag :div, h.link_to('My link', h.root_url)
  end

  private
  attr_reader :h
end

Your second option require all your unit tests to be stubbed as ActionController::Base.helpers which maybe isn't a good option and your third option you're using a huge context to access just some methods.

Upvotes: 7

Rudolf
Rudolf

Reputation: 1886

I would really make that dependent on what kind of methods you use. If it's just the basics like content_tag etc. I would go for the ActionController::Base.helpers way. It is also possible to call some helpers directly, e.g. for paths inside models I almost always use something along the lines of Rails.application.routes.url_helpers.comment_path.

For controller-specific stuff the third option might be useful, but personally the "pure" way seems nicer. Draper has an interesting approach too: They save the view_context for the current request and then delegate the calls to h-helpers to it: https://github.com/drapergem/draper/blob/master/lib/draper/view_context.rb

It really is just a matter of preference. I would never include all helpers at once, as you already said. But the second option is quite nice if you want to build the presentation layer yourself without using a gem like Draper or Cells.

Upvotes: 2

Related Questions