GithubHelp home page GithubHelp logo

Comments (31)

jcasimir avatar jcasimir commented on July 20, 2024

I think this is an easy fix, but I need to build a sample devise app and try it out. I'm an all-OmniAuth guy these days :)

from draper.

jaylevitt avatar jaylevitt commented on July 20, 2024

Cool. You shouldn't even need to build a devise app; just look at how devise uses helper_method, and create a helper the same way. (Which means you can even add a spec for it!)

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

I ran into this same issue, helpers which require access to controller.

Have you thought about sending the controller's view_context to the decorator instead of an all helpers module? It would give you all the proxies (request, controller, etc), and instead of all helpers, you'd get those available to the controller. Perhaps there's some obvious issue I'm not considering here?

from draper.

jcasimir avatar jcasimir commented on July 20, 2024

I have tried all kinds of hackery to figure out the current controller without having to pass it in during the decorator instantiation, but all efforts have so far failed.

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

I actually did this yesterday morning after making the comment above, essentially replacing helpers with the controller's view_context, but yeah, I am simply passing it along to the constructor, which does feel a bit clunky. It works fine so far though, giving me access to all the context I need. The examples use current_user but I don't know how that works without having access to the request context? Like jaylevitt says, it certainly doesn't work out of the box with devise, which expects controller to be defined as it depends on helper_method.

I wonder if storing the current controller in a before filter in a thread local variable would work?

from draper.

jaylevitt avatar jaylevitt commented on July 20, 2024

I wonder if storing the current controller in a before filter in a thread local variable would work?

This is essentially what we're doing in our app. Icky, but it works:

class ApplicationController < ActionController::Base
  prepend_before_filter { @@current_controller = self }
end

class YourPresenter
  def current_user
    ApplicationController.current_controller.current_user
  end
end

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

I'll probably end up combining this with my prior view_context hack, providing access to the current controller through a similar variable set each request, probably using Thread.current, then rather than building out a helpers module, simply grabbing that controller's view_context instead.

Essentially:

def helpers
  ApplicationController.current.view_context
end

I just wonder, is this somehow against the design philosophy of this gem?

from draper.

jcasimir avatar jcasimir commented on July 20, 2024

ApplicationController.current.view_context would be awesome, but does it exist? I couldn't find ApplicationController.current in the API and it didn't work in my experiment.

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

Sorry didn't mean to be confusing there. It doesn't exist, it's just something I wrote as an example to what I was trying to do. I did implement it this afternoon, along with my override of helpers above, and so far everything is working well (not backed by any tests yet though).

# In a module BaseController that I mix into ApplicationController
def BaseController.current
  Thread.current['controller']
end

def BaseController.current=(controller)
  Thread.current['controller'] = controller
end

# in ApplicationController
before_filter { BaseController.current = self }

# in my top level subclass of Draper::Base
def helpers
  @helpers ||= BaseController.current.view_context
end
alias :h :helpers

Working perfectly for me so far.

from draper.

jcasimir avatar jcasimir commented on July 20, 2024

Oh shit, I think I understand what you're doing here. This could be a really excellent idea that solves multiple problems. I need to experiment more.

from draper.

jfelchner avatar jfelchner commented on July 20, 2024

+1

If that actually works, it should enable button_to, image_tag, form_for and all of the other tags that are currently unavailable in the Decorator models. @numbers1311407 that's very clever stuff. Are there any potential gotchas with Thread.current?

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

No gotchas so far. I've been using this approach for a few months now and it's all worked fine. That's essentially the entirety of the code in the above comment.

The view_context not only gives you access to the request context, but it also ensures you have the specific helpers available to the controller.

from draper.

jcasimir avatar jcasimir commented on July 20, 2024

I just released 0.8.0 using this technique. Would you let me know if it helps the original issue?

from draper.

jfelchner avatar jfelchner commented on July 20, 2024

@jcasimir so it looks like this may actually work but it broke something else. The way the code is currently written, it's saying that there will definitely be a view_context at all times. This is fine when running the app, but not fine at all when testing. Since, when you're testing you want to be running the decorator methods in isolation.

What ends up happening is that, in the decorator's unit tests, #helpers returns nil.

I see one of two options, either:

  1. a conditional is added so that if view_context is empty, it reverts to ApplicationController::all_helpers. This is an okay solution but seems to be patching the code just to run the tests. Not horrible but not ideal.
  2. figure out a way, in the decorator's specs, give it a stubbed view context so it has access to everything it needs.

Thoughts?

from draper.

samuelhwong avatar samuelhwong commented on July 20, 2024

I'm getting some odd behaviour: h.current_user works as expected but only on the first access. When I view the page as a signed-in user it's fine, but then I sign out, the value of h.current_user remains. If I restart thin, it resets...

from draper.

jfelchner avatar jfelchner commented on July 20, 2024

Ok, I decided to go with #2 above. This is what I've come up with so far and it seems to be working okay after about 20 minutes of messing with it:

class DecoratorView < ActionView::Base
  include Rails.application.routes.url_helpers
  include ApplicationHelper

  def default_url_options
     {
      :host => #my host:port here,
      :port => #my port here
     }
  end
end

Then in my spec all I have to do is:

Thread.current[:current_view_context] = DecoratorView.new

The source for this was a SO question here: http://stackoverflow.com/questions/4262044/rails-3-how-to-render-erb-template-in-rake-task

The only other problem with doing it this way is that all of the custom application helpers need to be included manually. @jcasimir can we use some of the code that was originally used in #helper on Draper::Base and utilize that here to include all of the helpers automatically?

My initial thought was to put this in the README but I'm concerned that the 'knowledge of the masses' won't be respected there. In other words, if there's a problem, people are likely to change it in their app and not modify the README to be correct.

Now I'm thinking it'd be better off as DraperActionView or something which people can include in their specs. If there's a problem, it's more likely to garner an issue or PR this way.

from draper.

samuelhwong avatar samuelhwong commented on July 20, 2024

After implementing the code snippets by @numbers1311407, my issue is gone... thanks! It would be nice to have it work out of the box, though -- especially for newbie Rails devs like myself. ;)

from draper.

jcasimir avatar jcasimir commented on July 20, 2024

Arg, I see some issues that the changes have brought up. I will do my best to carve out some time tomorrow night.

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

@jcasimir: I think the issue @rocketscientist was having with the persisting current_user can be solved by simply not trying to cache the view_context. It's set with ||= now, which would be problematic because the thread doesn't die after it's done handling a request.

from draper.

esdras avatar esdras commented on July 20, 2024

@numbers1311407 Yup, you're right, I had the same issue and "Thread.current[:current_view_context] = self.view_context" seems to solve the problem. I'm not familiar with the project though, Am I missing something?

from draper.

jfelchner avatar jfelchner commented on July 20, 2024

More notes regarding testing the decorators in isolation:

To use anything which requires configuration variables from the controller, you'll need to create a controller instance and pass that to your ActionView instance. Adding on to what I mentioned above, you would have:

class Draper::ApplicationController < ApplicationController
  def url_options
    {
      :host     => <whatever_your_host_is>,
      :protocol => 'http'
    }
  end
end

controller = Draper::ApplicationController.new
Thread.current[:current_view_context] = Draper::ActionView.new(nil, {}, controller)

Using this, you should be able to use things like image_tag without a problem in your decorator specs.

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

@jfelchner What if you went at this from the opposite approach, delegating to a stubbed controller for view_context rather than instantiating a view manually? This more closely approximates the actual code and thus feels less artificial. It also solves a problem not yet noted: the absence of a request.

# something like...
before_my_tests do
  controller = WhateverController.new
  controller.request = ActionDispatch::Request.new('HTTP_HOST' => 'whateverhost.com')
  Thread.current[:current_view_context] = controller.view_context
end

from draper.

jcasimir avatar jcasimir commented on July 20, 2024

Looks like we've got this covered.

from draper.

jfelchner avatar jfelchner commented on July 20, 2024

@numbers1311407 Awesome. I'm not very familiar with how all that stuff is wired up. Your solution worked perfectly. Thank you!

from draper.

fpellanda avatar fpellanda commented on July 20, 2024

For me, this works, quick and dirty:

class ApplicationDecorator < Draper::Base
  def helpers
    @helpers ||= super.instance_variable_get("@_controller").view_context
  end
  alias :h :helpers
end

from draper.

e9digital avatar e9digital commented on July 20, 2024

@fpellanda I don't understand what's happening here. I don't know which version you're using but in current versions super already does this work and is the view_context, no? Effectively this method would be saying:

def helpers
  view_context.controller.view_context
end

from draper.

fpellanda avatar fpellanda commented on July 20, 2024

As far as I can see in my source code (draper 0.9.5)

module Draper
  class Base
  ....
  class << self
    def helpers
      Draper::ViewContext.current
    end
    alias :h :helpers
  end
end

And this would return:

Thread.current[:current_view_context]

Which has, in my application, has not the local variables from the controller set.

But your right, this would be nicer:

def helpers
  Draper::ViewContext.current.controller.view_context
end

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

The point is that Draper::ViewContext.current _is_ the view_context for the current controller. It's set in a before_filter each request. Your override retrieves it, then retrieves its controller, which in turn returns the selfsame view_context, effectively doing nothing.

from draper.

fpellanda avatar fpellanda commented on July 20, 2024

I know they should, but they are really not the same:

I'm using Rails 3.1.3 and draper 0.9.5.

How to reproduce:

rails new testapp; cd testapp
# Add this to gemfile: gem "draper"
bundle update
rails generate scaffold post title:string body:text published:boolean
rails generate draper:decorator Post
rake db:migrate

remove javascript stuff from application layout (gave me an error)

Add to post_decorator.rb

def title
  ("<br>" + helpers.instance_variables.inspect +
    "<br>" + Draper::ViewContext.current.controller.view_context.instance_variables.inspect).html_safe
end

Decorate @post in show action of posts_controller.rb:

def show
  @post = Post.find(params[:id])
  @post = PostDecorator.decorate(@post)
  ...

When you now create a post and goto show, you will see in the title, that the
the two objects are not the same. @post is missing.

My output of http://127.0.0.1:3000/posts/1 :

Title:
[:@_config, :@view_renderer, :@_routes, :@_assigns, :@_controller, :@_request, :@view_flow, :@output_buffer, :@virtual_path]
[:@_config, :@view_renderer, :@_routes, :@_assigns, :@_controller, :@_request, :@view_flow, :@output_buffer, :@virtual_path, :@post]
...

This means, if you want to use @post in a helper, it will be nil.

from draper.

numbers1311407 avatar numbers1311407 commented on July 20, 2024

This is a good catch. The problem you're seeing happens because the view_context itself is a snapshot, built on the fly.

def view_context
  view_context_class.new(lookup_context, view_assigns, self)
end

In most cases it's a non-issue because, well, the intention of helpers isn't to function as the view_context. The view_context helpers method was introduced to give access to the request context and controller-defined helpers.

In other words, if your decorators are dependent on action-specific instance variables, you're probably doing it wrong (in this case, @post is the model, and you'd access it as such).

_That being said_, the current implementation is problematic, if only because it introduces the possibility for inconsistent behavior based on filter order.

Perhaps it should only store a reference to the current controller, rather than caching a view_context snapshot, then helpers itself could invoke and cache view_context.

Something like my original suggestion:

# in the before filter
Draper::ViewContext.current_controller = self

# and
def helpers
  @helpers ||= Draper::ViewContext.current_controller.view_context
end

from draper.

fpellanda avatar fpellanda commented on July 20, 2024

Yes I know that I shouldn't use @post in the helper - this was just an example, in my case I set an instance variable in a before filter to change behaviour in some helper methods.

from draper.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.