Search code examples
ruby-on-rails

Rails named yield won't work unless I add another non-named yield above


I have a partial that I am trying to render content inside of

<!-- app/views/shared/_yes_no_toggle.html.erb -->
<div data-controller="yes-no-toggle">
  <div class="flex items-center">
    <button type="button" data-action="click->yes-no-toggle#onYes" class="bg-blue-500 text-white px-4 py-2 rounded-l rounded-r-none text-xs" aria-labelledby="yes-no-label">Yes</button>
    <button type="button" data-action="click->yes-no-toggle#onNo" class="bg-red-500 text-white px-4 py-2 rounded-r rounded-l-none text-xs" aria-labelledby="yes-no-label">No</button>
    <label class="ms-2" id="yes-no-label">Include Core Funded Services?</label>
  </div>

  <div data-target="yes-no-toggle.yesView" class="mt-4 hidden">
    <%= yield :yes_content %>
  </div>
  <div data-target="yes-no-toggle.noView" class="mt-4 hidden">
    <%= yield :no_content %>
  </div>
</div>

However, when I try to render this view within another rendered view, the named <%= yield :yes_content %> won't work unless I add another yield tag on top:

<!-- app/views/shared/_yes_no_toggle.html.erb -->
...
  <div data-target="yes-no-toggle.yesView" class="mt-4 hidden">
    <%= yield %>
    <%= yield :yes_content %>
  </div>
  <div data-target="yes-no-toggle.noView" class="mt-4 hidden">
    <%= yield %>
    <%= yield :no_content %>
  </div>
...

This is being called inside another partial

<!-- app/views/shared/example/_form.html.erb -->
<%= render 'shared/yes_no_toggle' do %>
  <% content_for :yes_content do %>
    <span>Hello World</span>
  <% end %>

  <% content_for :no_content do %>
    <span>Goodbye World</span>
  <% end %>
<% end %>

Can someone help me understand what's happening here? I have gone through the documentation to understand how it may be working but I'm a bit stumped. I've tried adding the content_for below the proposed render but no luck.


Solution

  • Without plain yield the block that you pass to render 'shared/yes_no_toggle' is never executed which means content_for is never populated with anything.

    The actual render is a lot more complicated, this is as simple as I could make it in terms of actual logic underneath:

    def content_for name
      @content_for ||= {}
      if block_given?
        @content_for[name] = yield
      else
        @content_for[name]
      end
    end
    
    def render template
      # calls _yes_no_toggle method
      send(template) do |content|
        if content
          content_for(content)
        else
          yield if block_given?          # here <---------------.
        end                              #                      |
      end                                #                      |
    end                                  #                      |
                                         #                      |
    def _yes_no_toggle                   #                      |
      # NOTE: without yielding >-------------.                  |
      # yield                            #   |                  |
                                         #   |                  |
      "<div>" +                          #   |                  |
        yield(:yes_content).to_s +       #   |                  |
        yield(:no_content).to_s +        #   |                  |
      "</div>"                           #   |                  |
    end                                  #   |                  |
                                         #   |                  |
    def _form                            #   |                  |
      render :_yes_no_toggle do          # <-' this block       |
        content_for :yes_content do      #     is never called -'
          "<span>Hello World</span>"     #
        end                              #
        content_for :no_content do       #
          "<span>Goodbye World</span>"   #
        end                              #
      end
    end
    
    # without yield
    _form
    #=> "<div></div>"
    
    # with yield
    _form
    #=> "<div><span>Hello World</span><span>Goodbye World</span></div>"
    

    Also, remember that yield(:yes_content) is the same as content_for(:yes_content) maybe that will clear up confusion with plain yield.