Within a Plain Old Ruby Object (PORO) in my rails app: I have the following method:
def some_method
content_tag(:li, link_to("Do something", somewhere_path(object.id)))
end
First: the object didn't understand the method content_tag, so I added the following which made the object understand that method:
include ActionView::Helpers::TagHelper
Then the object didn't understand link_to so I added the following which made the object understand that method:
include ActionView::Helpers::UrlHelper
Now, it doesn't understand my route: somewhere_path(object.id).
Question: How can I make the PORO in my rails app understand the helpers which generate routes?
Followup Question: Is there an easier way to include all of this functionality into my PORO object? Perhaps there is a way to only include one major module and get all of this functionality (as opposed to perhaps needing to require 3 different modules).
You either have to do what you describe in your self-answer (link to revision I refer to), or inject some context into your POROs. Where context is something which knows all those methods. Something like this:
class ProjectsController
def update
project = Project.find(params[:id])
presenter = Presenters::Project.new(project, context: view_context) # your PORO
# do something with presenter
end
end
And your PORO would look like this:
module Presenters
class Project
attr_reader :presentable, :context
def initialize(presentable, context:)
#presentable = presentable
#context = context
end
def special_link
context.somewhere_path(presentable)
end
end
end
Me, I like neither of them. But sometimes we have to choose a lesser evil.
If anyone happens to know of a current way to get access to all of these methods with one include statement then let me know.
Why, yes. There is a way.
module MyViewCompatibilityPack
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
def url_helpers
Rails.application.routes.url_helpers
end
end
class MyPoro
include MyViewCompatibilityPack
...
end
The issue is that actionview-related methods are not available to POROs.
In order to get all the great stuff from actionview: you need to utilize the view_context keyword. Then: you can simply call upon actionview-related methods from your view_context:
class BuildLink
attr_accessor :blog, :view_context
def initialize(blog, view_context)
#blog = blog
#view_context = view_context
end
def some_method
content_tag(:li, link_to(“Show Blog“, view_context.blog_path(blog)))
end
end
So for example: from your controller you would call upon this PORO like so:
BuildLink.new(#blog, view_context).some_method
For more information, see below references:
Rails doc on view_context
Utilization of view_context via presenter pattern, shown in this article
Railscast which talks through utilizing view_context via presenter pattern
Related
I have some helpers that are defined on runtime that are specific for a single call, e.g. a single instance of a controller (the next call could have different helper methods). Is there a robust way to add a helper method to an instance of a controller and it's view only, without adding the helper to other instances and views of this controller?
To define a helper for ALL instances, you could use the .helper_method method, e.g.
class Article < ApplicationController
helper_method :my_helper
def my_helper
# do something
end
end
I digged around in the source code, and found the (fairly private looking) #_helpers method which returns a module that contains all helpers for this instance. I could now use some meta programming to define my methods on this module
def index
_helpers.define_singleton_method(:my_helper) do
# do something
end
end
But I don't like this approach because I'm using a clearly private intended method that could easily change in the future (see the leading _).
If I only needed the helper inside the controller instance only, I could just call #define_singleton_method on the instance directly, but this doesn't make it available to the view.
So I'm looking for an official "Rails way" to define a helper for a single instance of a controller and it's view, like Rails provides with it's class method .helper_method.
I'm not sure if there is an official Rails way of doing this.
You could create an anonymous module and extend from that. Since this solution uses pure Ruby, you'll have to extend both the controller and view.
before_action :set_helpers, only: :index
def index
# ...
end
private
def set_helpers
#helpers = Module.new do |mod|
define_method(:my_helper) do
# do something
end
end
extend(#helpers)
end
<% extend(#helpers) %>
I've been researching the 'recommended' way to use Rails view helpers (e.g. link_to, content_tag) in a plain ruby class, such as a presenter. It seems there's very little information on this front and I wanted to get an idea of what the Stack community thought.
So, the options we have are.. (note I'm using Rails 4, and am less concerned about older versions)
Include the required modules manually
This is probably the cleanest way, since only the helpers needed are included. However I have found this method to not work in some cases, as the usual view context provided in plain Rails helpers is configured for the current request. url_for wouldn't know about the current request for example, so the host might not match.
class MyPresenter
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::CaptureHelper
def wrapped_link
content_tag :div, link_to('My link', root_url)
end
end
Use ActionController::Base.helpers
Since Rails 3, ActionController::Base has included a helpers method to access the current view context. I believe the view context provided by this method is configured as it would be in a rails helper, but I might be wrong. There's not really any documentation about this which seems worrying, but it does work quite well in practice.
class MyPresenter
def wrapped_link
h.content_tag :div, h.link_to('My link', h.root_url)
end
protected
def h
ActionController::Base.helpers
end
end
I believe this view context can also be mixed in with include, but the rails view helpers have hundreds of methods and it feels dirty to include them all indiscriminately.
Inject the view context when calling the presenter
Finally, we could just pass the view context to the class when it's initialized (or alternatively in a render method)
class MyPresenter
attr_accessor :context
alias_method :h, :context
def initialize(context)
#context = context
end
def wrapped_link
h.content_tag :div, h.link_to('My link', h.root_url)
end
end
class MyController < ApplicationController
def show
# WARNING - `view_context` in a controller creates an object
#presenter = MyPresenter.new(view_context)
end
end
Personally I tend to lean towards the latter two options, but with no definitive answer from the Rails team (that I've been able to find) I felt a bit unsure. Who better to ask than Stack!
I would go with the mix of the second and third option, something like:
class MyPresenter
def initialize(helpers)
#h = helpers
end
def wrapped_link
h.content_tag :div, h.link_to('My link', h.root_url)
end
private
attr_reader :h
end
Your second option require all your unit tests to be stubbed as ActionController::Base.helpers which maybe isn't a good option and your third option you're using a huge context to access just some methods.
I would really make that dependent on what kind of methods you use. If it's just the basics like content_tag etc. I would go for the ActionController::Base.helpers way. It is also possible to call some helpers directly, e.g. for paths inside models I almost always use something along the lines of Rails.application.routes.url_helpers.comment_path.
For controller-specific stuff the third option might be useful, but personally the "pure" way seems nicer. Draper has an interesting approach too: They save the view_context for the current request and then delegate the calls to h-helpers to it: https://github.com/drapergem/draper/blob/master/lib/draper/view_context.rb
It really is just a matter of preference. I would never include all helpers at once, as you already said. But the second option is quite nice if you want to build the presentation layer yourself without using a gem like Draper or Cells.
I'm trying to implement Decorators using the learnings from "Rails 4 Patterns" Code School course, but I'm running into trouble as I need a view helper in the Decorator class.
I want my view to have:
<%= #model_decorator.previous %>
Then in the decorator:
def previous
if object.prev_item.nil?
"Previous"
else
link_to("Previous", object)
end
end
The course suggests you make a call to the decorator within your view helper in the view file itself, but that's no good if the logic could output one result with a helper and one without. (i.e. need the output to be a link or not).
I've tried using helpers.link_to but it errors out as not providing the correct information for the url_for option. I've confirmed link_to("Previous", object) works within the view itself.
For Rails 4
include ActionView::Helpers::UrlHelper
link_to("Previous", Rails.application.routes.url_helpers.send("#{object.class.name.underscore}s_path".to_sym, object))
As for me it`s better to make a decorator for it:
class LinkDecorator
include ActionView::Helpers::UrlHelper
def initialize(label, object)
#label = label
#object = object
end
def show
link_to(label, url_helpers.send("#{object.class.name.underscore}s_path".to_sym, object))
end
def index
link_to(label, url_helpers.send("#{object.class.name}s_path".to_sym))
end
...
private
attr_reader :label, :object
def url_helpers
Rails.application.routes.url_helpers
end
end
Example usage:
LinkDecorator.new(object.name, object).show
If I understand your problem correctly, you essentially want links in a plain old ruby object.
My solution would be this:
include ActionView::Helpers::UrlHelper
link_to("Previous", Rails.application.routes.url_helpers.objects_path(object))
# assuming the object is always of one class
If the object is of a different class, than it would be possible to use the .send method to send the correct message to app ie.:
include ActionView::Helpers::UrlHelper
link_to("Previous", Rails.application.routes.url_helpers.send("#{object.class}s_path".downcase.to_sym, object))
# I'd create a function out of that line to make it a bit neater
It sounds like the error thrown by url_for comes from missing the routes and there's a few ways to include those. My solution kinda avoids that problem by using Rails.application.routes.url_helpers. Hope this helps!
In my Rails 3 application I use Ajax to get a formatted HTML:
$.get("/my/load_page?page=5", function(data) {
alert(data);
});
class MyController < ApplicationController
def load_page
render :js => get_page(params[:page].to_i)
end
end
get_page uses the content_tag method and should be available also in app/views/my/index.html.erb.
Since get_page uses many other methods, I encapsulated all the functionality in:
# lib/page_renderer.rb
module PageRenderer
...
def get_page
...
end
...
end
and included it like that:
# config/environment.rb
require 'page_renderer'
# app/controllers/my_controller.rb
class MyController < ApplicationController
include PageRenderer
helper_method :get_page
end
But, since the content_tag method isn't available in app/controllers/my_controller.rb, I got the following error:
undefined method `content_tag' for #<LoungeController:0x21486f0>
So, I tried to add:
module PageRenderer
include ActionView::Helpers::TagHelper
...
end
but then I got:
undefined method `output_buffer=' for #<LoungeController:0x21bded0>
What am I doing wrong ?
How would you solve this ?
To answer the proposed question, ActionView::Context defines the output_buffer method, to resolve the error simply include the relevant module:
module PageRenderer
include ActionView::Helpers::TagHelper
include ActionView::Context
...
end
Helpers are really view code and aren't supposed to be used in controllers, which explains why it's so hard to make it happen.
Another (IMHO, better) way to do this would be to build a view or partial with the HTML that you want wrapped around the params[:page].to_i. Then, in your controller, you can use render_to_string to populate :js in the main render at the end of the load_page method. Then you can get rid of all that other stuff and it'll be quite a bit cleaner.
Incidentally, helper_method does the opposite of what you're trying to do - it makes a controller method available in the views.
If you don't want to include all of the cruft from those two included modules, another option is to call content_tag via ActionController::Base.helpers. Here's some code I recently used to achieve this, also utilizing safe_join:
helpers = ActionController::Base.helpers
code_reactions = user_code_reactions.group_by(&:code_reaction).inject([]) do |arr, (code_reaction, code_reactions_array)|
arr << helpers.content_tag(:div, nil, class: "code_reaction_container") do
helpers.safe_join([
helpers.content_tag(:i, nil, class: "#{ code_reaction.fa_style } fa-#{ code_reaction.fa_icon }"),
helpers.content_tag(:div, "Hello", class: "default_reaction_description"),
])
end
end
I could use some help on including and extending Ruby modules and classes.
My previous question handled the named routes, but not all of the view/tag helpers due to the default_url_options Hash. The issue here is that ActionController::UrlWriter methods, like url_for, call the class attribute default_url_options. So when including ActionController::UrlWriter it extends the current class singleton but also needs to extend the current class itself. If you look at my code below, MyBigClass should have the default_url_options on it's class, not instance. This works, but I'm not sure if it's correct or will potentially break something.
Here's my current module:
module MessageViewHelper
module Methods
def self.included(base)
base.module_eval do
include TemplatesHelper
include LegacyUrlsHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::UrlHelper
include ActionController::UrlWriter
end
MyBigClass.class_eval do
cattr_accessor :default_url_options
def self.default_url_options(options = {})
options.merge({:host=>'www.myhostname.com'})
end
end
unless ActionController::UrlWriter.method_defined?('old_url_for')
ActionController::UrlWriter.class_eval do
alias_method :old_url_for, :url_for
def url_for(options)
if options.is_a?(String)
options
else
old_url_for(options)
end
end
end
end # unless method_defined?
end
end
end
class MyBigClass < ActiveRecord::Base
def message(template_name)
class << self; include MessageViewHelper::Methods; end
# ... more stuff here
end
end
I know I'm not entirely clear on ruby class/module design and extensions. Does anyone have any insight on this? Should the changes on MyBigClass be reverted at the end of message?
Calling include from within a class method or a class_eval block will bring the included module's definitions into the the class itself. I don't completely understand what you're trying to do, but based on your description, I think you're doing it correctly.
MyBigClass.class_eval do
cattr_accessor :default_url_options
def self.default_url_options(options = {})
options.merge({:host=>'www.myhostname.com'})
end
end
Normally, a class_eval defines regular instance methods on the class.
But, in this case inside the class_eval block you are defining the method on self. self inside the block is MyBigClass, so it would actually create a method in MyBigClass's singleton class. Any method in the singleton class is not an instance method.