开发者

Block call in Ruby on Rails

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 st开发者_如何学Cructure 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!


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"]


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(..)


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. :-)


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜