4

I'm trying to clean up my code and get rid of a lot of ugly hashes. In my views I define several actions like this:

@actions = {
  :interest => {'Show interest', link_to(..), :disabled => true},
  :follow   => {'Follow this case', link_to(..)}
  ...
}

As these hashes grow, the maintainability decreases. I want to convert the above format to something like:

actions do
   item :interest, 'Show interest', link_to(..), :disabled => true
   item :follow,   'Follow',        link_to(..)
   ...
end

How do I structure my helper methods to allow this? Preferably the 'item'-method should only be available in the 'actions' block and not in the global scope.

Thanks!

Mohsen Nosratinia
  • 9,844
  • 1
  • 27
  • 52
Mattias
  • 41
  • 1
  • 2

4 Answers4

3

i think this technique is called a 'clean room', where you have an anonymous object that contains the method you want to call so that the method is only available from within your block:

def actions(&block)
  cleanroom = Class.new{
    def item(*args)
      puts "these args were passed in: #{args.inspect}"
    end
  }
  cr = cleanroom.new
  cr.instance_eval &block
end

of course this "item" method just puts some text, but you cando whatever you need.

actions do
  item "foo", "bar", "baz"
end  #=> these args were passed in: ["foo", "bar", "baz"]
Derick Bailey
  • 72,004
  • 22
  • 206
  • 219
  • Thanks. How would I access the @actions property from within both the item method and the actions method, so I can pass it on to the view? – Mattias Jun 16 '10 at 12:15
  • actually, i think Konstantin's solution is better. mine would need some major rework to be usable in a controller – Derick Bailey Jun 16 '10 at 14:18
1

I wanted to do similar things, and ended up with a complex but very helpful class I named DslProxy. It's part of my iron-extensions gem, but you're welcome to pull it out and use it, or take a look at it and see how it works.

The docs for DslProxy are here: http://rubydoc.info/gems/iron-extensions/1.1.2/DslProxy

The github repo is here: https://github.com/irongaze/iron-extensions

Basically, doing this right is hard. As others have noted, instance_eval, which is generally very nice for metaprogramming, loses the calling context/binding, and so you lose your instance variables. Things get even more hairy if you want to nest these builder calls.

Here's a sample of what my DslProxy can do:

class ItemBuilder
  def actions(&block)
    @actions = []
    DslProxy.exec(self, &block)
    @actions
  end

  def item(*args)
    @actions << Item.new(*args)
  end
end

# ... in your view ...
<%
  @times = 5
  builder = ItemBuilder.new
  builder.actions do
    item :foo, link_to(...)
    @times.times do
      item :bob, link_to(...)
    end
  end
%>

The calling context is preserved (eg the link_to calls work), the instance vars are propagated (eg @times is available), and the methods that the ItemBuilder instance defines are available without explicit receiver (eg calls to item work as expected).

This, like all metaprogramming, is complex. You might find it helpful to look at the spec for this class here: https://github.com/irongaze/iron-extensions/blob/master/spec/extensions/dsl_proxy_spec.rb

Feel free to contact me with questions, or post issue to my github tracker. :-)

Irongaze.com
  • 1,639
  • 16
  • 22
1

Here is a similar solution, actually creating your data structure and avoiding the creation of a new class on every call of actions:

def action
  class << @actions ||= {}
    def item(name, *args) self[name] = args end
  end
  @actions.instance_eval(&Proc.new) if block_given?
  @actions
end

You can now use the dsl to construct that structure:

actions do
  item :interest, 'Show interest', link_to(..), :disabled => true
end

actions # => { :interest => [ 'Show interest', link_to(..), :disabled => true ] }

actions.item :follow, 'Follow', link_to(..)
Konstantin Haase
  • 25,687
  • 2
  • 57
  • 59
  • It looks like the instance_eval(&Proc.new) results in helper methods not being available, eg. link_to: undefined method `link_to' for {}:Hash – Mattias Jun 17 '10 at 20:42
  • Yeah, that is because `instance_eval` changes `self`. You can do `yield(@actions)` and use it like `actions { |a| a.item }` instead. – Konstantin Haase Jun 18 '10 at 23:26
0

I experimented a bit and ended up with a solution that works so far:

def actions(&block)
   @actions ||= []
   def item(id, content = '', options = {})
     @actions << [id, {
       :content => content || ''
     }.merge(options)]
   end
   block.call
end

Which in terms allows me to do the following in my views:

actions do
  item :primary, link_to('Write letter', ...), :disabled => true
end

And the @actions-variable is filled with these values.

Mattias
  • 41
  • 1
  • 2