dry-view: how to define a helper method, with an arg, in view class which can be invoked from view template

Let’s assume following is my view

require "dry/view"

class ArticlesListingView < Dry::View
  config.paths = [File.join(__dir__, "templates")]
  config.part_namespace = Parts
  config.layout = "application"
  config.template = "articles/index"

  attr_reader :article_repo

  def initialize(article_repo:)
    @article_repo = article_repo
    super
  end

  expose :articles do
    article_repo.find_all
  end
end

and following is the template (implemented using HAML) rendered by that view

- articles.each do |article_obj|
  %h1= article_obj.title
  %div<
     %a{ href: "/articles/#{article_obj.id}/edit" } Edit

Now I would like to move the edit link construction to a helper method in my ArticlesListingView class like following

def article_edit_link_path(article_id:)
   "/articles/#{article_id}/edit"
end

so that in the template I can use it in following manner

- articles.each do |article_obj|
  %h1= article_obj.title
  %div<
     %a{ href: article_edit_link_path(article_id: article_obj.id) } Edit

So how can I achieve that?

I tried experimenting with using exposures in following shown 3 ways for achieving that but it didn’t worked:

  expose :article_edit_link_path do |article_id: nil|
     construct_article_edit_link_path(article_id)
  end
  
  expose :article_edit_link_path do |article_id:|
     construct_article_edit_link_path(article_id)
  end

  expose :article_edit_link_path do |article_id|
     construct_article_edit_link_path(article_id)
  end

  private

  def construct_article_edit_link_path(article_id:)
     "/articles/#{article_id}/edit"
  end

Which makes me ask the question are exposures meant for the purpose I mentioned or there is a different provision in dry-view for achieving the need at hand?

Thanks.

An exposure is for passing values into the view, not behavior. You want to use Context in this case.

@alassek Thanks for your reply and recommendation of defining the helper method in Context class. I explored more around it and trying to understand the runtime workflow found that when a template gets rendered by following code

a scope is passed while rendering (ref: dry-view/renderer.rb at main · dry-rb/dry-view · GitHub) and thus checking out Dry::View::Scope implementation found that if no scope class is explicitly defined for a view then by default Dry::View::Scope (L63 in this class implementation) should be used and any methods invoked in the template not found in the scope class should be looked for in Context class through method_missing defined at L155 in Dry::View::Scope class.

Now in my understanding usually a Context should remain same for all views in an application so instead of bloating the Context class with desired behaviour helpers for views of different nature, it is better to use custom scope class for a specific view. That custom scope class should encapsulate the helpers relevant only for a desired view . For e.g. I tried following and it worked:

views/articles/scopes/listing.rb

require "dry/view/scope"

module Views
  module Articles
    module Scopes
       class Listing < Dry::View::Scope
          private
  
          def article_edit_link_path(article_id:)
             "/articles/#{article_id}/edit"
          end
       end
    end
  end
end

views/articles/listing.rb

require "dry/view"

module Views
  module Articles
    class Listing < Dry::View
      config.paths = [File.join(__dir__, "templates")]
      config.layout = "application"
      config.template = "articles/index"

      # Note this
      config.scope = Scopes::Listing
    
      attr_reader :article_repo
    
      def initialize(article_repo:)
        @article_repo = article_repo
        super
      end
    
      expose :articles do
        article_repo.find_all
      end
    end
  end
end

And then in the view I could successfully use article_edit_link_path(article_id:) I defined in the Views::Articles::Scopes::Listing scope class defined and configured specifically for Views::Articles::Listing.

Thanks.

If it helps, this pattern is pretty much universal across all templating systems that I’m familiar with.

Consider how ERB does the same thing. Every template system that executes code accepts a string template, and a binding context.

Dry::View::Scope is a formalized way of customizing your binding context when the template renders.

1 Like