ruby-on-railsrubyyieldinstance-eval

How do I refactor code that uses `instance_exec` several times?


I'm working on a class that generates a PDF using Prawn gem. I have some similar methods. All of them start with the same line. Here is the code:

module PDFGenerator
  class MatchTeamInfo
    include Prawn::View

    def initialize(match)
      @match = match
      @output = Prawn::Document.new page_layout: :landscape
      defaults
      header
      footer
    end

    def render
      @output.render
    end

    def defaults
      @output.instance_exec do
        font_size 16
        text 'hola'
      end
    end

    def header
      @output.instance_exec do
        bounding_box [bounds.left, bounds.top], :width  => bounds.width do
          text "Fulbo", align: :center, size: 32
          stroke_horizontal_rule
          move_down(5)
        end
      end
    end

    def footer
      @output.instance_exec do
        bounding_box [bounds.left, bounds.bottom + 25], :width  => bounds.width do
          stroke_horizontal_rule
          move_down(5)
          text "Tu sitio favorito!", align: :center
        end
      end
    end
  end
end

Is there a way to avoid @output.instance_exec in every method and use something like blocks? I tried it, but I can't get it work. Can I do something like this?

def apply
  @output.instance_exec do
    yield
  end
end

How am I supposed to define the code blocks?


Solution

  • First of all, you need to make all helper methods to return lambda instance:

    def defaults
      lambda do
        font_size 16
        text 'hola'
      end
    end
    

    Now you might pass lambdas returned by your helpers to instance_exec. To acknowledge it about “this is code block rather than regular param,” lambda is to be prefixed with ampersand:

    def apply
      #                     ⇓ NB! codeblock is passed!
      @output.instance_exec &defaults
    end
    

    If you want to pass a codeblock to apply, you should re-pass it to instance_exec. Unfortunately I know no way to re-pass it using yield keyword, but here is a trick: Proc.new called without parameters inside a method that was called with a codeblock given, is instantiated with this codeblock, so here you go:

    def apply
      raise 'Codeblock expected' unless block_given?
      @output.instance_exec &Proc.new
    end