Dynamic Fragment Caching with Escaped ERb

So maybe this has been done before, but this is something I wrote that I
found useful.

Many of the ‘fragments’ I wanted to cache differed only by some small
value like the :id of the :url, but because this changed every request,
the options were to either store a fragment for every :id, or to not
cache.

So I wrote a helper I call ‘dynamic_cache’. It works by using escaped
Erb INSIDE of an ERb fragment, and calling render :inline on the cached
fragment before returning.

So for instance:

<% cache do %>
<%= link_to_remote ‘My Link’, :url => { :id => @my_id } %>
<% end %>

This doesn’t cache, since the :id is variable.

So with dynamic_cache it would look like the following:

<% dynamic_cache do %>
<%= link_to_remote ‘My Link’, :url => { :id => ‘<%= @my_id %>’ } %>
<% end %>

@my_id doesn’t get evaluated and the escaped ERb delimiters get cleaned
up to just ‘<%= @my_id %>’. The cache then calls render :inline on the
cached fragment which evaluates any of the newly un-escaped ERb with the
current values at the time of the render.

Add the following to your application.rb for this to work…

module ActionController #:nodoc:
module Caching
module Fragments
def dynamic_cache_erb_fragment(block, name = {}, options = nil)
# this grabs the _erbout variable from the blocks scope
buffer = eval(’_erbout’, block.binding)
if ! cache = read_fragment(name, options)
# by default the block.call will write everything to the
_erbout
# this is an ugly way to get around that, and is the same
method used in capture_erb_with_buffer
# but it is only called the first time, so not a big deal
pos = buffer.length
block.call
cache = buffer[pos…-1]
buffer[pos…-1] = ‘’
# save it to the cache if we are caching
write_fragment(name, cache, options) if perform_caching
end
buffer.concat(render :inline => cache)
end
end
end
end

module ActionView
module Helpers
module CacheHelper
def dynamic_cache(name = {}, &block)
@controller.dynamic_cache_erb_fragment(block, name)
end
end
end
end

You can also do some more complicated caching than would otherwise be
allowed (ie, caching larger blocks of code…it gets a little ugly, but
may be useful.

In the following example, condtional is executed at the time of
rendering, while the links are cached.

<% dynamic_cache do %>
<%= ‘<% if @my_test_value == “HURRAY” %>’ %>
<%= link_to “My Test2”, “HURRAY” %>
<%= ‘<% else %>’ %>
<%= link_to “My Test3”, “HIDEYHO” %>
<%= ‘<% end %>’ %>
<% end %>

PLEASE NOTE: THERE IS A BUG HERE CURRENTLY I NEED TO FIX. If you use
more than one dynamic_cache request you get the a double render error.
I need to figure out how to get the effect of render :inline with
actually using render.

OK, here is a fixed version (just added ‘@template.’ to the render :

module ActionController #:nodoc:
module Caching
module Fragments
def dynamic_cache_erb_fragment(block, name = {}, options = nil)
# this grabs the _erbout variable from the blocks scope
buffer = eval(’_erbout’, block.binding)
if ! cache = read_fragment(name, options)
# by default the block.call will write everything to the
_erbout
# this is an ugly way to get around that, and is the same
method used in capture_erb_with_buffer
# but it is only called the first time, so not a big deal
pos = buffer.length
block.call
cache = buffer[pos…-1]
buffer[pos…-1] = ‘’
# save it to the cache if we are caching
write_fragment(name, cache, options) if perform_caching
end
logger.error cache
buffer.concat(@template.render(:inline => cache))
end
end
end
end

module ActionView
module Helpers
module CacheHelper
def dynamic_cache(name = {}, &block)
@controller.dynamic_cache_erb_fragment(block, name)
end
end
end
end

Here is a fixed example of conditionals. Notice the use of double
quotes to wrap the escaped ERb.

<% dynamic_cache do %>
<%= “<% if @my_test_value == ‘BOOOO’ %>” %>
<%= link_to “My Test”, “/<%= @my_test_value %>” %>
<%= “<% else %>” %>
<%= link_to “My Test”, “/<%= @my_test_value %>2” %>
<%= “<% end %>” %>
<% end %>

Obviously, another option would be to use a separate delimiter and
replace it with ‘<%’ and ‘%>’ in the dynamic_cache method when it
initially creates the cache record.

Like,

<% dynamic_cache do %>
<%= “#% if @my_test_value == ‘BOOOO’ %#” %>
<%= link_to “My Test”, “/#%= @my_test_value %#” %>
<%= “#% else %>” %#>
<%= link_to “My Test”, “/#%= @my_test_value %#>2” %>
<%= “#% end %#>” %>
<% end %>

You would just need to cache.gsub for ‘%#’ and ‘#%’ before
write_fragment.

Sorry, left a logging statement in there, which shouldnt be:

module ActionController #:nodoc:
module Caching
module Fragments
def dynamic_cache_erb_fragment(block, name = {}, options = nil)
# this grabs the _erbout variable from the blocks scope
buffer = eval(’_erbout’, block.binding)
if ! cache = read_fragment(name, options)
# by default the block.call will write everything to the
_erbout
# this is an ugly way to get around that, and is the same
method used in capture_erb_with_buffer
# but it is only called the first time, so not a big deal
pos = buffer.length
block.call
cache = buffer[pos…-1]
buffer[pos…-1] = ‘’
# save it to the cache if we are caching
write_fragment(name, cache, options) if perform_caching
end
buffer.concat(@template.render(:inline => cache))
end
end
end
end

module ActionView
module Helpers
module CacheHelper
def dynamic_cache(name = {}, &block)
@controller.dynamic_cache_erb_fragment(block, name)
end
end
end
end