Reload dynamically defined helpers - ruby-on-rails

I want to define view helpers in Rails 5.2.0 on runtime (from within some code that lies within my lib folder and / or some initializer) and I came up with this approach so far:
def new_module
Module.new do
def self.create_method(name, &block)
define_method(name, &block)
end
end
end
def define_dynamic_helper(name, &block)
helpers = new_module
helpers.create_method(name, &block)
ActionView::Base.send :include, helpers
end
Now that I can define dynamic modules that get include into ActionView::Base on runtime, I call them e.g. in my controller like this:
define_dynamic_helper("my_helper") do
"some data"
end
And my view uses the helper like this
<%= my_helper %>
But this has a drawback during development: When I remove the line that defines my helper, it is still available but I would expect a MethodMissing error. And as you can guess, this can lead to very complicated situations to debug.
So I got two questions here:
Is it possible to completely remove all dynamic helpers when Rails does a reload during development? Is there some kind of hook I can use?
Is using ActionView::Base.send :include, helpers the right approach for this? Or is there another entry point that I could use (which maybe provides a better reloading approach?)

Related

Recommended way to use Rails view helpers in a presentation class

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.

Using Decorator pattern in Rails but can't access view helper

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!

Logic for Application Layout

I am trying to make an ActiveRecord call to get information for the application layout (the default application.html.haml layout). I know I am not supposed to put logic into the layout itself, but I am unsure of where to put it.
The line of code I need run in the layout is just a simple Model call:
Client.find_by(:id => current_user.client_id)
I would suggest throwing it in helpers/application_helper.rb. I've used this in the past for things such as title helpers and body class helpers.
# helpers/application_helper.rb
module ApplicationHelper
def body_class
[controller_name, action_name].join(' ')
end
end
# views/layouts/application.slim
body class=body_class
= yield
The ApplicationController isn't for such helpers. It's mainly as support for your controllers, not your views.
It's okay if you put it in ApplicationController. And you can put controller related code to controllers/concerns folder.
'concerns/concern.rb':
module Concern
def method
# Your code here
end
end
To use a module from concerns folder include it in the controller: include Concern

Add Facebook metadata to Rails pages with sub-pages

I've tried Facebook's Open Graph protocol in adding meta data on Rails pages. What I want to do now is to make my code not duplicated or D.R.Y.---instead of putting one meta-data header for each controller page I have, I'd like to create a base class called "MyMetaBuilder" which will be inherited by the sub-pages, but don't know where and how to start coding it...
Someone suggested that meta data property values must be dynamically generated depending on the context. For example, PlayMetaBuilder, CookMetaBuilder and so on...
Also, when unit testing the controller action, how do I verify for its existence?
Thanks a lot.
One thing is defining the tags, another is rendering them. I would do the following:
write a controller mixin (something like acts_as_metatagable) where I would define specific fields for each controller (and populate the remaining with defaults). These would be assigned to a class (or instance) variable and in this way be made accessible in the rendering step).
write an helper function which would take all my tags and turn them into html. This helper function would then be called in the layout and be rendered in the head of the document.
so, it would look a bit like this:
# homepage_controller.rb
class HomepageController < ActionController::Base
# option 1.2: include it directly here with the line below
# include ActsAsMetatagable
acts_as_metatagable :title => "Title", :url => homepage_url
end
# lib/acts_as_metatagable.rb
module ActsAsMetatagable
module MetatagableMethods
#option 2.2: insert og_tags method here and declare it as helper method
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
def self.included(base)
base.helper_method :og_tags
end
end
def acts_as_metagabable(*args)
include MetatagableMethods
# insert dirty work here
end
end
# option 1.1: include it in an initializer
# initializers/acts_as_metatagable.rb
ActiveController::Base.send :include, ActsAsMetatagable
# option 2.1: insert og_metatags helper method in an helper
module ApplicationHelper
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
end
What I did for Scoutzie, was put all metadata into a head partial, with if/else cases as such:
%meta{:type => 'Author', :content => "Kirill Zubovsky"}
%meta{'property' => "og:site_name", :content=>"Scoutzie"}
-if #designer
...
-elsif #design
...
-else
...
This way, depending on the variables that load, I know which page it is, and thereby know which metadata to include. This might not be an elegant solution, but it works and it's really simple.

Ruby/Rails: alias_method practices

I'm trying to override Rails' "fields_for" method, which I'm currently doing as follows:
module ActionView::Helpers::FormHelper
include ActionView::Helpers::FormTagHelper
alias_method :original_fields_for, :fields_for
def fields_for(<my arguments>)
# Some extra stuff
# ...
output.safe_concat original_fields_for(<my other arguments>)
end
end
The functionality works just fine, but I'm starting to suspect that my use of alias_method isn't the most elegant. Most especially, if I were to package this functionality into a gem, and there were another gem that overrode fields_for, am I write in thinking either my new fields_for OR the alternate fields_for would be skipped?
Assuming so, what's the correct way to go about slapping in some extra functionality to an existing rails method?
Cheers...
this seems like exactly the situation that alias_method_chain is meant for (although I don't know offhand if it will work on a Module - have only used it on AR::Base)
You'd just do
module ActionView::Helpers::FormHelper
include ActionView::Helpers::FormTagHelper
alias_method_chain :fields_for, :honeypot
def fields_for_with_honeypot(<my arguments>)
# Some extra stuff
# ...
output.safe_concat fields_for_without_honeypot(<my other arguments>)
end
end
interesting idea to do this to fields_for, but it should work.
There is a minor controversy around a_m_c you should be aware of - this post sums it up well http://erniemiller.org/2011/02/03/when-to-use-alias_method_chain/
In this case, I don't think you can use super because you want to monkey-patch form_for without modifying the calling code/views.

Resources