Reputation: 516
I'm extending Prawn-Rails with PrawnRailsForms. I want those methods to only be available within a block that gives them some context. For example, usage of my extension might look like:
field_row height: 25, units: 8 do
text_field field: 'Favorite fruit', # ...
end
The way this works under the hood in my library is that the field_row
method instantiates a helper object, without which the other methods, like text_field
, don't run:
class FieldRow
# ...
end
class PrawnRails::Document
attr_accessor :field_row # my helper object
def field_row(height:, units:, &block)
@field_row = FieldRow.new #...
block.call
@field_row = nil
end
def text_field(**args)
unless @field_row.present?
raise ArgumentError, 'Must be inside a field row'
end
# implementation which relies on @field_row...
end
end
Is there a nicer way to make sure that my methods are only run when the FieldRow
object is available? In particular, having to write @field_row = nil
after block.call
makes me feel like I'm doing something incorrectly.
Upvotes: 1
Views: 265
Reputation: 106027
Is there a nicer way to make sure that my methods are only run in the proper context, e.g. when the
FieldRow
object is available?
APIs like this usually use instance_eval
so that the methods inside the block are called on the instantiated object:
module PrawnRailsForms
class FieldRow
def initialize(height:, units:, **)
# ...
end
def text_field(field:, **)
# ...
end
end
module Document
def field_row(**args, &block)
@field_row = FieldRow.new(**args)
@field_row.instance_eval(&block) unless block.nil?
@field_row
end
end
::PrawnRails::Document.include(Document)
end
Now when you do this:
doc.field_row(height: 25, units: 8) do
text_field field: 'Favorite fruit'
end
...it will instantiate a FieldRow object with height:
and units:
, then call text_field
on that object.
If you need FieldRow to operate on the document itself, there are a few approaches you could take. One is to just pass self
(the document object) to FieldRow's constructor and store it in an instance variable therein for use by text_field
et al:
def field_row(**args, &block)
row = FieldRow.new(self, **args)
row.instance_eval(&block) unless block.nil?
end
You could also define a method on FieldRow to which you pass the document object. In this scenario, FieldRow's methods (e.g. text_field
) "configure" the output and render
(or whatever you choose to call it) actually creates the output.
def field_row(**args, &block)
row = FieldRow.new(**args)
row.instance_eval(&block) unless block.nil?
row.render(self)
end
I would caution against adding a bunch of methods to Document. Since text_field
only works inside the context of field_row
, making text_field
an instance method on Document doesn't really make sense (and, as you've discovered, has some gotchas).
Upvotes: 2