Question

Caching is by far the most logic-intensive part of my view code, so I would like to do fragment caching from inside a decorator, however, I cant do it.

When i do this from my decorator:

def cached_name
  h.cache do
   "a name here"
  end
end

I get this:

You have a nil object when you didn't expect it! You might have expected an instance of Array. The error occurred while evaluating nil.length

I instantiate my decorator from inside a controller

@presenter = SomePresenter::new

I am using HAML for my views

How can I succesfully cache from inside my decorator, so my view can do stuff like this

= @decorator.cached_logic_heavy_stuff

UPDATE: I have created a git repo showing my issue: https://github.com/houen/presenter_caching

UPDATE: This maybe works - see the repo

  include Haml::Helpers
  def another_way_to_try
    self.init_haml_helpers
    buffer = haml_buffer.buffer

    h.with_output_buffer(buffer) do
      h.cache do
        h.concat "i should still not be empty"
      end
    end
  end
Was it helpful?

Solution

Rails' cache method tries to infer a cache key based on the view that it's being called from. Since you're not actually calling it from a view (but from inside an instance of a decorator class), I expect that it's bombing when trying to build a cache key.

You might try passing a cache key explicitly, via h.cache "your cache key" do. With a full stack trace, you can figure out where it's throwing the exception, and then work around that, as well. Without the full stack trace, it's harder to help you, though.

Edit: Looking at Rails' caching code, I think this might be a deeper issue; it's attempting to get the length of output_buffer, which isn't going to be available outside of your views' contexts (that is, within Draper). You might try adding:

def output_buffer
  h.output_buffer
end

But without testing it, I'm thinking it might not work exactly as planned without some more work. This is just a rough guess - I'd be surprised if this is actually the issue, but hopefully it gets you on the right path.

The note in the source there:

# VIEW TODO: Make #capture usable outside of ERB
# This dance is needed because Builder can't use capture

indicates that this isn't a fully-solved problem, so you may need to do a little digging around in the Rails internals to make this one work.

OTHER TIPS

I'd suggest using Rails.cache directly might solve your problem; we do the same thing in our decorators with Rails 4.

def cached_name
  Rails.cache.fetch(source) do
    source.name # etc.
  end
end

If you're using Draper, I believe you don't need to explicitly pass the view context. You will likely want to pass a model or collection to your draper present when you instantiate. Examples:

class UserDecorator < Draper::Base
  decorates :user

  # additional methods
end

# in the controller
@presenter = UserDecorator.new(@user)       # for an instance
@presenter = UserDecorator.decorate(@users) # for a collection

I suspect the nil object error you're getting is coming from another method call that's not listed in your code.

As for fragment caching from your decorator, you'll want to use the concat helper method to get this to work inside the decorator:

# your decorator class
def cached_name
  h.cache("some_cache_key") do
    h.concat "a name here"
  end
end

This works:

  include Haml::Helpers
  def another_way_to_try
    self.init_haml_helpers
    buffer = haml_buffer.buffer
    h.with_output_buffer(buffer) do
      h.cache "some_key10", :expires_in => 10.seconds do
        h.concat "i should still not be empty 2"
      end
    end
  end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top