Ruby on Rails: Using XML Builder Partials Ruby on Rails: Using XML Builder Partials xml xml

Ruby on Rails: Using XML Builder Partials


I worked around this by passing in the builder reference as a local in the partial. No monkey patching needed. Using the original example:

xml.foo do     xml.id(foo.id)     xml.created_at(foo.created_at)     xml.last_updated(foo.updated_at)     foo.bars.each do |bar|         render(:partial => 'bar/_bar', :locals => {:builder => xml, :bar => bar })     end end

Then in your partial make sure to use the 'builder' object.

builder.bar do  builder.id bar.idend

Also, the above appears to only work up to Rails 4. Rails 5 and up see @srghma's comment below


There is unfortunately not a straight-forward solution to this. When looking at the code that ActionPack will initialize the Builder object with then the indent size is hard-coded to 2 and the margin size is not set. Its a shame that there is no mechanism to override this at present.

The ideal solution here would be a fix to ActionPack to allow these options to be passed to the builder but this would require some time investment. I have 2 possible fixes for you. Both dirty you can take your pick which feels less dirty.

Modify the rendering of the partial to render to a string and then do some Regex on it. This would look like this

_bar.xml.builder

xml.bar do  xml.id(bar.id)  xml.name(bar.name)   xml.created_at(bar.created_at)   xml.last_updated(bar.updated_at)end

foos/index.xml.builder

xml.foos do  @foos.each do |foo|    xml.foo do      xml.id(foo.id)      xml.name(foo.name)      xml.created_at(foo.created_at)      xml.last_updated(foo.updated_at)      xml.bars do        foo.bars.each do |bar|          xml << render(:partial => 'bars/bar',                  :locals => { :bar => bar } ).gsub(/^/, '      ')        end      end    end  endend

Note the gsub at the end of render line. This produces the following results

<?xml version="1.0" encoding="UTF-8"?><foos>  <foo>    <id>1</id>    <name>Foo 1</name>    <created_at>2010-06-11 21:54:16 UTC</created_at>    <last_updated>2010-06-11 21:54:16 UTC</last_updated>    <bars>      <bar>        <id>1</id>        <name>Foo 1 Bar 1</name>        <created_at>2010-06-11 21:57:29 UTC</created_at>        <last_updated>2010-06-11 21:57:29 UTC</last_updated>      </bar>    </bars>  </foo></foos>

That is a little hacky and definitely quite dirty but has the advantage of being contained within your code. The next solution is to monkey-patch ActionPack to get the Builder instance to work the way we want

config/initializers/builder_mods.rb

module ActionView  module TemplateHandlers    class BuilderOptions      cattr_accessor :margin, :indent    end  endendmodule ActionView  module TemplateHandlers    class Builder < TemplateHandler      def compile(template)        "_set_controller_content_type(Mime::XML);" +          "xml = ::Builder::XmlMarkup.new(" +          ":indent => #{ActionView::TemplateHandlers::BuilderOptions.indent}, " +          ":margin => #{ActionView::TemplateHandlers::BuilderOptions.margin});" +          "self.output_buffer = xml.target!;" +          template.source +          ";xml.target!;"      end    end  endendActionView::TemplateHandlers::BuilderOptions.margin = 0ActionView::TemplateHandlers::BuilderOptions.indent = 2

This creates a new class at Rails initialisation called BuilderOptions whose sole purpose is to host 2 values for indent and margin (although we only really need the margin value). I did try adding these variable as class variables directly to the Builder template class but that object was frozen and I couldn't change the values.

Once that class is created we patch the compile method within the TemplateHandler to use these values.

The template then looks as follows :-

xml.foos do  @foos.each do |foo|    xml.foo do      xml.id(foo.id)      xml.name(foo.name)      xml.created_at(foo.created_at)      xml.last_updated(foo.updated_at)      xml.bars do        ActionView::TemplateHandlers::BuilderOptions.margin = 3            foo.bars.each do |bar|          xml << render(:partial => 'bars/bar', :locals => { :bar => bar } )        end        ActionView::TemplateHandlers::BuilderOptions.margin = 0      end    end  endend

The basic idea is to set the margin value to the indentation level that we are at when rendering the partial. The XML generated is identical to that shown above.

Please do not copy/paste this code in without checking it against your Rails version to ensure that they are from the same codebase. (I think the above is 2.3.5)


Maybe you should do:

xml.foo do  xml.id(foo.id)  xml.created_at(foo.created_at)  xml.last_updated(foo.updated_at)  xml.bars do    foo.bars.each do |bar|      xml.bar bar.to_xml # or "xml.bar render(:xml => bar)"                         # or "xml.bar render(bar)" (loads bar/_bar partial)    end  endend

Have a look at this link about the xml builder.

In the last alternative you could replace the inner loop with:

xml.bars render(foo.bars) # will loop over bars automatically using bar/_bar

You can probably also try:

xml << foo.to_xml(:include => :bars)

if you want to include all fields in the result.

I'm not sure about the indention of all these, so you may need to fall back to create the contents of the inner loop the same way as you do in the outer block, eg using no partial.