Understanding yield in rails presenter

2019-08-06 11:40发布

问题:

In 'Rails View' book that I'm reading, there is code about presenters

app/helpers/designers_helper.rb

module DesignerHelper
  def designer_status_for(designer = @designer)
    presenter = DesignerStatus.new(designer)
    if block_given?
      yield presenter
    else
      presenter 
    end
  end
end

then in show.html.erb

<% designer_status_for do |status| %>
   some html using <%= status %> variable

<% end %>

I'm having hard time understanding how the block of code gets the variable status assigned and why do we need 'if' statement in the helper function and why not just return the presenter variable directly?

回答1:

Let's start with the view

<% designer_status_for do |status| %>
   "some html using <%= status %> variable"    
<% end %>

This calls the method designer_status_for with a block of code as shown below

designer_status_for {|status| "some html using <%= status %> variable" }

In the method,

presenter = DesignerStatus.new(designer)
#=>since no `designer` is given it takes the default value of `@designer` 
#  and finds the related presenter

if block_given?
#=> is true since a block is given 

yield presenter
#=> here the method "yields" to the block 
#   i.e. passes the presenter as a variable to the block and executes it. 
#   For e.g. array.map{|x| x+1} works the same way
#   `map` iterates over the array and yields each element to the given block.

{|status|   "some html using <%= status %> variable"}
#=> status is the variable received from method which is presenter
#   This block returns the html to the method that called it

end 
#=>the last statement was yield which was equal to the html returned from block
#  returns the html received from the block 

The if statement in the method is to return the result of block of code if given else it returns the presenter. This allows for DRY code where we need presenter variable in bunch of places in a block of code.



回答2:

Ok, lets do this step by step.

Firstly, you define a helper-method which assigns @designer as default. This is the reason, why you can call it without any arguments in your view:

<% designer_status_for do |status| %>
  # more code

Then, DesignerStatus is the actual presenter, which is a different class than Designer and creates a total new object, but which is sort of based on the @designer object. So, presenter is a new object of type DesignerStatus

block_given?, as you might guess, checks, if this object should be passed to a block or if it should work as a normal object. This is most likely the tricky part where you are having trouble understanding. So let me explain it the other way around. You can call something like this

designer_status_for.some_presenter_method

in your view, and it would work. It works, because no block is given and a designer object is returned (in other words, the else part is triggered), on which you can call some_presenter_method directly. So, you could use that all throughout your view, and you would be just fine.

However, there is an even better option, namely yield this object into a block. The yield option is what makes this code-snippet

<% designer_status_for do |status| %>
  status.some_presenter_method
<% end %>

possible in the first place. What this basically says is "take that presenter object, assign it to a variable called status and make this variable available to the block.

So, if you are having a hard time to wrap your head around yield right now, don't worry to much about it. Just call any method on designer_status_for itself. The benefit of using the block over the object itself is the fact, that your code becomes less brittle, but that's sort of another topic.



回答3:

After posting this question, I tried to re-read the code and try some samples in irb and here are findings which answer my question.

In Ruby any method can take a block of code. For eg.

def method_can_take_block?
  answer = 'yes it can take block of code'
end

method_can_take_block? { puts 'this code is not executed but gets passed to method' }

to have my block of code execute, it needs to be yielded inside the method. Yielding block of code can be done by just calling yield inside the method. By explictly calling yield we can control the line in which the block of code gets executed.

def method_can_take_block?
  answer = 'yes it can take block of code'
  yield
end

method_can_take_block? { puts 'this code gets executed after the variable 'answer' gets assigned with some value.' }

However we don't make any use of the variable 'answer' here. the code block passed doesn't have any access to the variable defined inside the function. So to give the block some access to the variables defined inside the method, we need make the block accept some argument. Note the use of |my_answer| in the code block below.

   method_can_take_block? { |my_answer| 100.times { puts answer } }

Now that we have added an argument to the block to the block, we need to pass this argument while calling the block of code. So this is done by calling yield with additional arguments.

def method_can_take_block?
  answer = 'yes it can take block of code'
  yield answer
end

yield answer passes answer variable to the block of code that is passed to the method. In this case, answer is passed as my_answer variable inside the block which then prints it 100 times.

With this understanding, my answer to questions

  1. how the block of code gets the variable status assigned ans: by the line yield presenter. Here presenter variable is passed on to the block which receives the argument in the name status.

  2. why do we need 'if' statement in the helper function and why not just return the presenter variable directly? We could well avoid passing the entire erb as block to the method but then we need have a code like this at the top of the show.html.erb:

<% status = designer_status_for(@designer) %> and then start using the variable status. I think it's a matter of aesthetics to use block so that we can avoid using an assignment. The if statement in the method is there to use this kind of syntax rather using block.