How to override sprockets paths in rails? - ruby-on-rails

I'm trying to override the path that Sprockets produces in Rails, particularly for the sass helpers.
I've gotten somewhere using various solutions:
a) overriding the image_tag helpers in the views
b) overriding ActionView::AssetPaths compute function
c) including new module in Sass::Script::Functions
But it feels like there should be a much simpler way than doing all this. Is there a nice way
to decorate the sprockets paths? I'm doing this because we have some custom url and packaging stuff happening.

So, in the end I found this message buried in sprockets/context.rb
Custom asset_path helper is not implemented
Extend your environment context with a custom method.
environment.context_class.class_eval do
def asset_path(path, options = {})
end
end
Which would seem to suggest that Rails.application.assets.context_class.class_eval would
allow me to change the asset_path handler, but it didn't seem to work. I'm not sure if that's my fault or not, as I couldn't find any examples of this.
Currently my hack to get this working is this:
ActionView::AssetPaths.class_eval do
def compute_public_path(source, dir, options = {})
my_transform(source)
end
end
Sprockets::Helpers::RailsHelper::AssetPaths.class_eval do
def compute_public_path(source, dir, options = {})
my_transform(source)
end
end
ActionView::Helpers::AssetTagHelper::AssetPaths.class_eval do
def compute_public_path(source, dir, options = {})
my_transform(source)
end
end
I suspect I don't need all three of these, but I'm not sure. Anybody who knows their sprockets want to comment on this?

Related

Why can't I override #asset_path from ActionView?

I am working on upgrading an app to Rails 5, and #asset_path now raises if the url is nil. I'm trying to monkey patch that method with a version that will work like Rails 4 so that I can get my tests passing.
I've spent hours on this, and I'm going crazy. For some reason no matter what I do, I can't monkey patch the module. I thought this initializer would work:
module ActionView
module Helpers
module AssetUrlHelper
alias asset_path_raise_on_nil asset_path
def asset_path(source, options = {})
return '' if source.nil?
asset_path_raise_on_nil(source, options)
end
end
end
end
I also tried putting my method in another module and includeing, prepending, and appending it to both ActionView::Helpers::AssetUrlHelper and ActionView::Helpers::AssetTagHelper.
No matter what I do, I can't get my method to be executed. The only way I can alter the method is to bundle open actionview and changing the actual method.
I figured out that it is because #asset_path is simply an alias. I needed to override the method which the alias points at:
module ActionView
module Helpers
module AssetTagHelper
alias_method :path_to_asset_raise_on_nil, :path_to_asset
def path_to_asset(source, options = {})
return '' if source.nil?
path_to_asset_raise_on_nil(source, options)
end
end
end
end

Reload dynamically defined helpers

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?)

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.

Extend existing Rails Path helper

I am wondering if there is a way to extend the existing path helpers that Rails created for my routes.
I have something like /videos/view/:id already and now I need to append a tag parameter to that link wherever it (from the current params collection).
The only solution I see right now is to go through all the views and change the call to the helper to look like this:
view_videos_path(video, tag: params[:tag])
Now obviously that's a bit of work and a much easier way to do this would be to just overwrite the existing path helper with something like this:
def view_videos_path(video, opts)
view_videos_path(video, opts.merge(tag: params[:tag]))
end
Obviously putting this in a module would result in a endless recursion so I wonder if there is any best practice on how to do this.
Also, what do you think of the approach? I am not really sure if extending the helper like this is wise or not. But to me at the moment it looks reasonable.
Throw them in a helper module and call super, you can also include it into ApplicationController so the paths are also available in your controllers:
# app/helpers/path_helpers.rb
module PathHelpers
def view_videos_path(video, opts)
super(video, opts.merge(tag: params[:tag]))
end
end
class ApplicationController
include PathHelpers # we could also use helper_method for each method
end

Custom Form Elements in Rails

So I'm new to Rails and I'm trying to figure out what the canonical way to add custom form elements is. Currently the way I'm doing it is super grotty.
module ActionView
module Helpers
module FormOptionsHelper
def some_new_field(object, method, options = {}, html_options = {})
#code code
end
end
class FormBuilder
def contract_year_select(method, options = {}, html_options = {})
#template.some_new_field(#object_name, method, objectify_options(options), #default_options.merge(html_options))
end
end
end
end
I have seen this however.
class Forms::ApplicationFormBuilder < ActionView::Helpers::FormBuilder
Forms::ApplicationHelper.instance_methods.each do |selector|
src = <<-end_src
def #{selector}(method, options = {})
#template.send(#{selector.inspect}, #object_name, method, objectify_options(options))
end
end_src
class_eval src, __FILE__, __LINE__
end
private
def objectify_options(options)
#default_options.merge(options.merge(:object => #object))
end
end
(from here)
Extending FormBuilder seems like a better solution than duck punching it. Is there some other way to do this that doesn't require directly copying parts of the FormBuilder class into my custom one?
Two parter here:
First, as someone new to rails, I would recommend using the Formtastic gem.
V is the abused and neglected part of MVC in rails.
Formtastic is very well tested and has support for Rails 3
https://github.com/justinfrench/formtastic
With Formtastic you can then create custom elements extending it's brilliant class and get a whole host of other benefits beyond simple field display.
Second, extending the class is definitely acceptable. I prefer the Module method as it's easier to look at. Like something along the lines of:
module Forms::ApplicationFormBuilder
class WhateverYourExtending
def some_special_method(stuff)
# all kinds of special stuff here
end
end
end
But I barely know what I am talking about here... This stuff is right on the edge of my understanding.
Just saw you talking about forms and I love Formtastic!

Resources