Handling ERB Syntax Changes for Form Helpers in Rails 3.1
When upgrading from Rails 3.0 to 3.1, one of the common issues we face is the breaking change in ERB syntax for helper methods. This change impacts form_tag
, form_for
, content_tag
, javascript_tag
, fields_for
, and field_set_tag
.
The main issue is that these helper methods in Rails 3.1 now require the use of <%= %>
to output content, whereas in Rails 3.0 (and earlier), they used <% %>
without needing to explicitly output the form content. This change is not backward-compatible, and applying it across a large codebase can be quite tedious when we are using our method of dual booting an application to do an upgrade.
In this article we will explore a few possibilities we have uncovered in the past few years while doing these upgrades.
First of all, let’s give an example of what the change looks like:
In Rails <= 3.0
<% form_for @user do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.submit "Save" %>
</p>
<% end %>
In Rails >= 3.1
<%= form_for @user do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.submit "Save" %>
</p>
<% end %>
You can find the relevant Rails commit here for more information.
To tackle this issue, we’ve explored a few different methods. Depending on the size of your application and the scope of changes required, one of these approaches may be right for your team. Let’s dive into them!
1. Branching Strategy for Dual Booting
One simple approach is to create a separate branch that includes all of the required changes for the Rails 3.1 syntax, and then merge that branch once dual booting is close to completion. This approach minimizes the use of conditionals or patches in your codebase, but does require extensive review and testing to ensure all views and helpers are correctly updated before the final merge.
This strategy works well when the changes are spread throughout the codebase, and you want to maintain clean code for both versions without constantly managing conditionals or workarounds. This may also work well for a smaller codebase when there aren’t so many changes in the upgrade that things become complicated.
Since we first came across this issue when doing a Rails 3.0 to 3.1 upgrade we have been trying to figure out what the best approach would be, recently, our colleague Ariel Juodziukynas, came up with some of the following ideas.
2. Conditional View Loading
Another approach is to create separate files for each of the views that require syntax changes, using a naming convention like *_3_0.html.erb
for Rails 3.0 and earlier. We created a script to check view files and add the second file if one of the form helpers existed in the file. You can then monkey patch ActionView to load the appropriate view based on the Rails version:
unless $next_rails
module ActionView
class PathSet
def find_template(original_template_path, format = nil, html_fallback = true)
return original_template_path if original_template_path.respond_to?(:render)
template_path = original_template_path.sub(/^\//, '')
each do |load_path|
if format && (template = load_path["#{template_path}_3_0.#{format}"])
return template
elsif template = load_path["#{template_path}_3_0"]
return template
elsif format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"])
return template
elsif format && (template = load_path["#{template_path}.#{I18n.default_locale}.#{format}"])
return template
elsif format && (template = load_path["#{template_path}.#{format}"])
return template
elsif template = load_path["#{template_path}.#{I18n.locale}"]
return template
elsif template = load_path["#{template_path}.#{I18n.default_locale}"]
return template
elsif template = load_path[template_path]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.default_locale}.html"]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}_3_0.html"]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}.html"]
return template
end
end
return Template.new(original_template_path) if File.file?(original_template_path)
raise MissingTemplate.new(self, original_template_path, format)
end
end
end
end
This patch ensures that your Rails 3.0 views are loaded when running under the older framework, while allowing you to keep the Rails 3.1-compatible views in place for the newer version. This approach works best when you have many views that require changes, but still need to support both versions during the transition period.
It also makes it simple to understand what needs to be removed when you are finished dual booting. One the problems with the approach is that if you have a lot of changes be added to your main branch it can become difficult to maintain the changes being added to both versions of the view files.
3. Monkey Patching Rails Helpers
We initially tried monkey patching Rails 3.0’s helpers to behave like they do in Rails 3.1 by overriding methods like form_tag
and form_for
. However, this approach does not work. The views are loaded and the syntax immediately breaks before the methods can be overridden.
However, we did find that we could monkey patch the Rails 3.1 to behave like Rails 3.0 and therefore we could have a backwards compatible and forward compatible syntax.
Example of the patch:
if $next_rails
module ActionView
module Helpers
# this allows us to use the `<% form_tag` syntax in 3.1
# we must remove this and add the `=` once 3.0 is removed
module FormTagHelper
alias original_form_tag form_tag
def form_tag(url_for_options = {}, options = {}, &block)
output_buffer << original_form_tag(url_for_options, options, &block)
""
end
end
module FormHelper
alias original_form_for form_for
def form_for(record, options = {}, &block)
output_buffer << original_form_for(record, options, &block)
""
end
end
end
end
end
The one issue we found with this approach is that it won’t work if the code is also using the syntax of assigning the helper method result to a variable (you can see what this looks like below). Doing it this way always puts the content in the output buffer and it isn’t possible to do something like <% my_form = form_for .... do %>
.
4. Using Temporary Variable Assignment to Trick ERB Syntax
Finally, we discovered another clever workaround while working on a recent project. If you’re using a helper method like field_set_tag
, you can trick ERB into behaving correctly by assigning its output to a variable before rendering:
<% fields = field_set_tag do %>
<%= form.text_field :name %>
<% end %>
<%= fields %>
In Rails 3.0, this approach tricks the system into allowing the output directive (<%= %>
) without breaking the syntax. This works well if your code already uses variable assignments for form or field helpers. However, if this pattern is not present in your code, implementing it everywhere could become cumbersome and potentially introduce confusing assignments throughout the codebase. Just like the other workarounds, be sure to clean this up once you’ve fully transitioned to Rails 3.1.
Conclusion
When upgrading from Rails 3.0 to 3.1, you’ll likely encounter issues with the ERB syntax change. Depending on the scale of your application and the effort you’re willing to put into maintaining dual compatibility, any of the above methods can help you navigate this tricky situation.
The key takeaway here is that while dual booting between versions of Rails can be challenging, it’s not insurmountable. Whether you choose to branch your changes, create view conditionals, or implement workarounds with variable assignments, there are ways to keep your upgrade process smooth and manageable.
Ready to upgrade Rails? Need help dealing with the upgrade? Let’s talk! !