ruby-on-railsrubyjbuilder

Why the json.partial! line in jbuiler template is not executed when passing locals using new hash syntax?


I have this line in my jbuilder template using the new hash syntax. It seems this line is not executed at all.

json.partial! 'comments', post:, comments:

If I modify the line back to the old syntax or add braces or brackets, it works.

json.partial!('comments', post:, comments:)
json.partial! 'comments', { post:, comments: }
json.partial! 'comments', post: post, comments: comments

Can anyone explain what happens here? Is it something related to the template compilation? Thanks.


Solution

  • Test template for simplicity:

    # app/views/test.json.jbuilder
    json.attr name:
    

    I don't know how to get to it any other way but you can inspect compiled source at: https://github.com/rails/rails/blob/v7.0.4.3/actionview/lib/action_view/template.rb#L282

    to see what code actually runs when template renders:

    >> ApplicationController.renderer.render(template: "test", locals: {name: "first"})
      Rendering test.json.jbuilder
    "          def _app_views_test_json_jbuilder__1585277848127427767_29960(local_assigns, output_buffer)\n            @virtual_path = \"test\";name = local_assigns[:name]; name = name;;__already_defined = defined?(json); json||=JbuilderTemplate.new(self); json.attr name:\n\n      json.target! unless (__already_defined && __already_defined != \"method\")\n          end\n"
    => TypeError # doesn't actually render
    

    formatted

    def _app_views_test_json_jbuilder__1161877154070863274_90100(local_assigns, output_buffer)
      @virtual_path = "test";               # ActionView
      name = local_assigns[:name];          # ActionView
      name = name;;                         # ActionView
      __already_defined = defined?(json);   # Jbuilder
      json||=JbuilderTemplate.new(self);    # Jbuilder
      json.attr name:                       # Your template
      json.target! unless (__already_defined && __already_defined != "method")
      #    ^ Jbuilder json output
    end
    

    If you don't have brackets around it, json.target! becomes the value for the name and your template returns whatever json.attr returns or json.partial! in your case.

    It's also fixable if you put a semicolon at the end of this line:
    https://github.com/rails/jbuilder/blob/v2.11.5/lib/jbuilder/jbuilder_template.rb#L277

    #                                                                            here v
    %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source};
    
    >> ApplicationController.renderer.render(template: "test", locals: {name: "first"})
    => "{\"attr\":{\"name\":\"first\"}}"
    

    A simplified version

    def json *args
      @buffer = [*@buffer, *args]
    end
    
    def target 
      @buffer&.join
    ensure
      @buffer = nil
    end
    
    # ^ this is action view/jbuilder
    # v this is jbuilder templates
    
    def good_template name = "first"
      json(name:)
      target
    end
    
    # same as doing:
    # `json name: target`
    def bad_template name = "first"
      json name:
      target
    end
    
    >> good_template
    => "{:name=>\"first\"}"
    >> bad_template
    => [{:name=>nil}]       # Array instead of a String, if we had a controller
                            # there would be a TypeError somewhere.                 
    

    So method hash: is still an iffy syntax and much less obvious in a template. Just use parenthesis:
    https://bugs.ruby-lang.org/issues/18396