Comments (31)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
+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.
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.
I just released 0.8.0 using this technique. Would you let me know if it helps the original issue?
from draper.
@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:
- a conditional is added so that if
view_context
is empty, it reverts toApplicationController::all_helpers
. This is an okay solution but seems to be patching the code just to run the tests. Not horrible but not ideal. - 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.
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.
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.
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.
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.
@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.
@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.
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.
@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.
Looks like we've got this covered.
from draper.
@numbers1311407 Awesome. I'm not very familiar with how all that stuff is wired up. Your solution worked perfectly. Thank you!
from draper.
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.
@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.
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.
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.
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.
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.
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)
- Create unique context for each item in a collection?
- Draper is incompatible with Ruby 3 HOT 5
- Draper changes the behaviour of url_for when using STI models
- Sending problematic method names (open, format) to decorated rails objects sends to Kernel (conflicts with ransack)
- undefined method `auto_link' for spec with type :decorator
- Generators don't work. HOT 3
- Draper::UninferrableDecoratorError (Could not infer a decorator for Document.) HOT 3
- CI: A test MAY fail with NameError: uninitialized constant ActionMailer::Base
- Release new version HOT 7
- Breaks respond_with in Rails API apps HOT 2
- ArgumentError using helpers with Ruby 3 HOT 4
- Mongo error -> Uninitialized constant ActiveRecord Did you mean? ActiveModel HOT 1
- Usage with `Turbo::Broadcastable` HOT 1
- undefined method `attributes' for nil:NilClass
- Automatically use decorated associations in delegate methods HOT 1
- Unexpected behavior when `frozen?` method is defined in model HOT 1
- Scaffold generator: Invalid path when using --model-name=Record
- Draper still actively maintained? HOT 10
- Rails - STI bug for ActiveRecord_Relation, works fine for a single record
- issue with using draper outside of controller/view context within a rake task or (active)job
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from draper.