Is this a good DRY method in rails? - ruby-on-rails

I don't want use a //= require_tree . (because it loads all the assets I have, which I also don't need) and don't want write each time javasctipt_include_tag("my_controller"). So I decided to do following:
module ApplicationHelper
def add_asset(*files)
puts "DEBUG: add: " + files.to_s
content_for(:html_head) do
if GtFe::Application.assets.find_asset(*files)
yield :asset_include_tag
end
end
end
def javascript(*files)
add_asset(*files) do
content_for :asset_include_tag
javascript_include_tag(*files)
end
end
def stylesheet(*files)
add_asset(*files) do
content_for :asset_include_tag
stylesheet_link_tag(*files)
end
end
end
So I use name named yields in helper methods and I have a main add_asset() method and two asset-specific methods. Is it a good way to do so? Or are any better solutions available?
Update:
From the rails docs:
For example, if you generate a ProjectsController, Rails will also add
a new file at app/assets/javascripts/projects.js.coffee and another at
app/assets/stylesheets/projects.css.scss. By default these files will
be ready to use by your application immediately using the require_tree
directive. See Manifest Files and Directives for more details on
require_tree.
You can also opt to include controller specific stylesheets and
JavaScript files only in their respective controllers using the
following: <%= javascript_include_tag params[:controller] %> or <%=
stylesheet_link_tag params[:controller] %>. Ensure that you are not
using the require_tree directive though, as this will result in your
assets being included more than once.
So the javascript_include_tag and stylesheet_link_tag are justified. But is it good so to do this yield staff to DRY?
Update2:
I landed with this code refinements:
module ApplicationHelper
def add_asset(asset_type, *files)
puts "DEBUG: add #{asset_type} files: #{files}"
content_for(:html_head) do
files.each do |file|
puts "DEBUG: now add #{asset_type}: #{file}"
if GtFe::Application.assets.find_asset(file)
yield(:asset_include_tag, file)
end
end
end
end
def javascript(*files)
add_asset("js", *files) do
content_for :asset_include_tag
javascript_include_tag
end
end
def stylesheet(*files)
add_asset("css", *files) do
content_for :asset_include_tag
stylesheet_link_tag
end
end
end
And then I can write in each view/layout so:
= javascript(params[:controller], "#{params[:controller]}_#{params[:action]}")

I think this is overkill.
If you don't like require full tree unordered, you can require them manually one by one.
//= js_file_a
//= js_file_b
Comparing with your solution:
you still need typing the file names by yourself.
def add_asset(*files)
Several unnecessary helpers added when the jobs can be done elsewhere easily.

Related

Loading a javascript and running it on a specific controller and specific action in rails

I have standard rails app format.
I have these controller:
class StaticPagesController < ApplicationController
def help
end
def about
end
end
and I have the file app/assets/javascripts/static_page.js.coffee
I want 2 things:
load this javascript ONLY when I am running one of the pages of the static_pages
be able to make different js calls depending on the specific action inside the controller:
the javascript file should look like:
//general javascript code
if (isThisHelpPage) {
//Run some help page code
}
if (isThisAboutPage) {
//Run some about page code
}
I assume that the first issue should be solved somehow with the app/assets/javascripts/application.js file by adding some rule like:
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree .
//= (isItStaticController? render static_page.js.coffee)
But I want to know how can I do that?
Unfortunately you can't add conditionals to the application js file as it is precompiled when the app is deployed.
There are two potential approaches that I can see working for you here:
1) Place all of your javascript in a single file as you have above then set the page in your code like this:
<script> page = "Help" </script> or <script> page = "About" </script>
your script could then become:
if(page === "Help") .... else if(page === "About") ....
2) the other option would be to create separate js files for each page, and then incorporate them via a yield :head block in your layout. In your help file it would look like this:
<% content_for :head %>
<%= javascript_include_tag 'help' %>
<% end %>
I personally favour the second approach in my apps.
I'll explain how I handle this. In my ApplicationController I have a method that runs from a before_filter on every request.
def prepare_common_variables
controller_name = self.class.name.gsub(/Controller$/, '')
if !controller_name.index('::').nil?
namespace, controller_name = controller_name.split('::')
end
#default_body_classes = ["#{controller_name.underscore}_#{action_name} ".downcase.strip]
#default_body_classes = ["#{namespace.underscore}_#{#default_body_classes.join}".strip] if !namespace.nil?
end
In app/views/layouts/application.html.erb I have the following
<body class="<%= yield :body_classes %> <%= #default_body_classes.join(' ') %>">
For your StaticPagesController, when the help action runs, this would generate the following <body> tag:
<body class="static_pages_help">
Next, I have a method like this in my app/assets/javascripts/application.js.erb
Array.prototype.diff = function(a) {
return this.filter(function(i) {return !(a.indexOf(i) > -1);});
};
var DEEFOUR = (function (deefour) {
deefour.Utility = (function (utility) {
utility.hasBodyClass = function() {
var args = Array.prototype.slice.call(arguments);
if (args.length === 0 || $('body').get(0).attr('class') == "") return false;
return args.diff($('body').get(0).attr('class').split(/\s/)).length == 0;
};
return utility;
}(deefour.Utility || {}));
return deefour;
}(DEEFOUR || {}));
Finally, in my equivalent of your app/assets/javascripts/static_page.js.coffee I will have something like this
$(function(){
if (!DEEFOUR.Utility.hasBodyClass('static_pages_help')) return;
// code for your help page
});
$(function(){
if (!DEEFOUR.Utility.hasBodyClass('static_pages_about')) return;
// code for your about page
});
This is nice because in your view
<% content_for :body_classes, :some_custom_class %>
or within a specific action
#default_body_classes << "some_other_custom_class"
you can conditionally add specific classes to match against in your Javascript.
// *both* 'static_pages_help' and 'some_other_class' are required
if (!DEEFOUR.Utility.hasBodyClass('static_pages_help') || !DEEFOUR.Utility.hasBodyClass('some_other_class')) return;
hasBodyClass(...) accepts an arbitrary # of arguments; just list them out. This is useful for things like a new and create action which you want the same Javascript to run for when a form fails to submit.
if (!DEEFOUR.Utility.hasBodyClass('some_controller_new', 'some_controller_create')) return;
It should be noted, prepare_common_variables needs a bit of tweaking as it only allows for a single namespace like SomeNamespace::TheController and not more like SomeNamespace::AnotherNamespace::TheController.
The best practice here is to use the content_for helper method. Kind of like described by #Adam, but you probably want to put a <%= yield :script_files %> in the bottom of your layout file, and then call
<% content_for :script_files do %>
<%= javascript_include_tag 'your_js_file' %>
<% end %>
from within the action you want it to be. You can do this even cleaner by following a similar approach as mentioned by Ryan Bates in an early Railscast:
module ApplicationHelper
def javascripts(paths)
content_for :script_files do
javascript_include_tag(paths.is_a?(Array) ? paths.join(',') : paths)
end
end
end
You can then, from your action view, just call <% javascripts 'your_js_file' %> and have that file included.
load this javascript ONLY when I am running one of the pages of the static_pages
First of all, asset pipeline compiles and creates static asset files offline, meaning that you can not create bundles dynamically based on the controller name during the serving of a user request (in production).
However, you can use a separate <%= javascript_include_tag "static_page_manifest.js" %> statement instead of (including the application.js) in the corresponding layout file (of StaticPages controller) to define certain javascripts for the specific views. For example, you can have a app/assets/javascripts/static_page_manifest.js:
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree .
//= static_page
Alternatively, if you want dynamic javascript module inclusion, you could use requirejs (requirejs-rails) etc.
be able to make different js calls depending on the specific action inside the controller
This could be achieved by checking an body tag id with a value depending on controller and action names! For example:
<body id="static_pages_<%= controller.action_name %>">
...
And then you can check this id value in the javascript bundle to execute certain code snippets.

How can I customize the active admin layout?

I need to customize the active admin layout, but how can I do it?
The active admin layout is not actually defined as a layout file, but is generated programatically. Placing a custom layout in the layout directory will therefore not actually override the default layout.
You can, however, monkey-patch or duck-punch the active admin layout methods inside your application.
The following will add an ie-specific stylesheet to the header:
module ActiveAdmin
module Views
module Pages
class Base < Arbre::HTML::Document
alias_method :original_build_active_admin_head, :build_active_admin_head unless method_defined?(:original_build_active_admin_head)
def build_active_admin_head
within #head do
meta :"http-equiv" => "Content-type", :content => "text/html; charset=utf-8"
insert_tag Arbre::HTML::Title, [title, active_admin_application.site_title].join(" | ")
active_admin_application.stylesheets.each do |path|
link :href => stylesheet_path(path), :media => "screen", :rel => "stylesheet", :type => "text/css"
end
active_admin_application.javascripts.each do |path|
script :src => javascript_path(path), :type => "text/javascript"
end
text_node csrf_meta_tag
text_node "<!--[if lt IE 7]>
<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"admin_ie7.css\ />
<![endif] -->".html_safe
end
end
end
end
end
end
Clearly an ugly solution.
When a view is defined in a gem AND in the rails app, the one defined in the Rails app is served. It's a logic priority.
So if you need to override all or some active admin views, you'll have to copy these in your app and change them as you desire.
Maybe ActiveAdmin does provide a nicer way to do this by now? I don't know.
However here would be an example for a bit cleaner patch for that situation, in my example to add the webpacker gems javascript_pack_tag to my admin area.
module MyApp
module ActiveAdmin
module Views
module Pages
module BaseExtension
def build_active_admin_head
super
within #head do
text_node(javascript_pack_tag('application'))
end
end
end
end
end
end
end
class ActiveAdmin::Views::Pages::Base < Arbre::HTML::Document
prepend MyApp::ActiveAdmin::Views::Pages::BaseExtension
end
(Using rails 5.1.4) I tried two solutions here that involved messing with the active_admin library, and they did not work for me at all. I found my solution in config/initializers/active_admin.rb. I am adding a small amount of bootstrap styling to the default layout. As far as linking to stylesheets, javascripts, etc., it was as simple as adding this to my active_admin.rb, as per the comments therein:
# == Register Stylesheets & Javascripts
#
# We recommend using the built in Active Admin layout and loading
# up your own stylesheets / javascripts to customize the look
# and feel.
#
# To load a stylesheet:
# config.register_stylesheet 'my_stylesheet.css'
config.register_stylesheet 'https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css', { integrity: 'sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk', crossorigin: 'anonymous' }
As far as editing that generated layout, I have yet to figure it out, but it could at least be done indirectly via JavaScript and the inclusion of that javascipt in this file via
config.register_javascript 'active_admin_view_tweaks.js', { defer: true }
I am going to be editing class attributes to make my pages responsive with bootstrap, so I might follow something like this geeksforgeeks article to edit the pages with JavaScript after they've loaded.
I don't know if there is a way to edit the generated layout more directly, but this should work for some cases.
You can override the active admin page layout by putting the following code in your config/intializers/active_admin.rb file:
module AdminPageLayoutOverride
def build_page(*args)
within super do
render "shared/your_custom_view_partial"
end
end
end
ActiveAdmin::Views::Pages::Base.send :prepend, AdminPageLayoutOverride
In the above example, I have a custom view file at app/views/shared/_your_custom_view_partial.html.erb location and I am injecting that in all of my active admin pages by the above code.

Rails 3.1 asset pipeline: how to load controller-specific scripts?

If I generate a new controller in Rails 3.1, also a javascript file with the name of the controller will added automatically. Firstly, I thought this javascript file will used only, when the related controller is called.
By default there is the instruction //= require_tree . in the application.js-file, that include every javascript file on it's tree.
How could I load only the controller specific script?
To load only the necessary name_of_the_js_file.js file:
remove the //=require_tree from application.js
keep your js file (that you want to load when a specific page is loaded) in the asset pipeline
add a helper in application_helper.rb
def javascript(*files)
content_for(:head) { javascript_include_tag(*files) }
end
yield into your layout:
<%= yield(:head) %>
add this in your view file:
<% javascript 'name_of_the_js_file' %>
Then it should be ok
An elegant solution for this is to require controller_name in your javascript_include_tag
see http://apidock.com/rails/ActionController/Metal/controller_name/class
<%= javascript_include_tag "application", controller_name %>
controller_name.js will be loaded and is in the asset also, so you can require other files from here.
Example, rendering cars#index will give
<%= javascript_include_tag "application", "cars" %>
where cars.js can contain
//= require wheel
//= require tyre
Enjoy !
I always include this inside my layout files. It can scope your js to action
<%= javascript_include_tag params[:controller] if AppName::Application.assets.find_asset("#{params[:controller]}.js") %>
<%= javascript_include_tag "#{params[:controller]}_#{params[:action]}" if AppName::Application.assets.find_asset("#{params[:controller]}_#{params[:action]}.js") %>
Your problem can be solved in different ways.
Add the assets dynamically
Please consider that this isn't a good solution for the production mode, because your controller specifics won't be precompiled!
Add to our application helper the following method:
module ApplicationHelper
def include_related_asset(asset)
# v-----{Change this}
if !YourApp::Application.assets.find_asset(asset).nil?
case asset.split('.')[-1]
when 'js'
javascript_include_tag asset
when 'css'
stylesheet_link_tag asset
end
end
end
end
Call the helper method in your layout-file:
<%= include_related_asset(params[:controller].to_param + '_' + params[:action].to_param . 'js') %>
Create specific assets for your controller actions. E. g. controller_action.js
Please don't forget to change YourApp to the name of your app.
Use yield
Add <%= yield :head%> to your layout head
Include your assets from your action views:
<% content_for :head do %>
<%= javascript_include_tag 'controller_action' %>
<% end %>
Please see the Rails guides for further information.
I like albandiguer's solution. With which I've found that javascript/coffeescript assets are not individually precompiled. Which causes all sorts of errors trying to use javascript_path. I'll share my solution to that problem after I address an issue a few people mentioned in his comments. Mainly dealing with only a partial set of controller named JavaScript files.
So I built an application helper to detect if the file exists in the javascript directory regardless of .coffee/.js extension:
module ApplicationHelper
def javascript_asset_path(basename)
Sprockets::Rails::Helper.assets.paths.select{|i|
i =~ /javascript/ and i =~ /#{Rails.root}/
}.each do |directory|
if Dir.entries(directory).map {|i| i.split('.')[0]}.compact.
include? basename
return File.join(directory, basename)
end
end
nil
end
end
This method will return the full path to the javascript file if it exists. Otherwise it returns nil. So following Pencilcheck's comment you can add this method for a conditional include:
<%= javascript_include_tag(controller_name) if javascript_asset_path(controller_name) %>
And now you have a proper conditional include. Now for the issue of precompiled assets. Generally for optimization you don't want assets precompiled individually. You can however do it if you must:
# Live Compilation
config.assets.compile = true
You can add this do your environment config file. Test it in your development environment file first. Again this is ill-advisable. The Rails asset pipeline uses Sprockets to optimize everything:
Sprockets loads the files specified, processes them if necessary,
concatenates them into one single file and then compresses them (if
Rails.application.config.assets.compress is true). By serving one file
rather than many, the load time of pages can be greatly reduced
because the browser makes fewer requests. Compression also reduces
file size, enabling the browser to download them faster.
PLEASE READ the documentation for further details of the mechanics of Sprockets (Asset Pipeline) http://guides.rubyonrails.org/asset_pipeline.html
Assets aren't precompiled individually. For example when I try:
<%= javascript_include_tag 'event' %>
I get:
Sprockets::Rails::Helper::AssetFilteredError: Asset filtered out and
will not be served: add Rails.application.config.assets.precompile +=
%w( event.js ) to config/initializers/assets.rb and restart your
server
So you can include which assets to be precompiled individually. We just need to add the relevant controller named javascript files in our asset initializer. Well we can do this programatically.
To get a list of controller names I will use ecoologic's example:
all_controllers = Dir[
Rails.root.join('app/controllers/*_controller.rb')
].map { |path|
path.match(/(\w+)_controller.rb/); $1
}.compact
And now to get the name of all javascript files that match the basename of the controller name you can use the following:
javascripts_of_controllers = Sprockets::Rails::Helper.assets.paths.select{|a_path|
a_path =~ /javascript/ and a_path =~ /#{Rails.root}/
}.map {|a_path|
Dir.entries(a_path)
}.flatten.delete_if {|the_file|
!the_file['.js']
}.collect {|the_file|
the_file if all_controllers.any? {|a_controller| the_file[a_controller]}
}
Then you can try:
# config/initializers/assets.rb
Rails.application.config.assets.precompile += javascripts_of_controllers
This will get you a list of all javascript files, without directory path, that match your controller name. Note if your controller name is plural, the javascript name should be as well. Also note if the controller is singular and the javascript file is plural this will still include it because of the_file[a_controller] will succeed on a partial match.
Feel free to try this out in your Rails.application.config.assets.precompile setting. I know that this gets you the list of files correctly. But I'll leave you to test it. Let me know if there are any nuances involved with precompiling this way as I am curious.
For a very thorough explanation on how assets precompile see this blog: http://www.sitepoint.com/asset-precompile-works-part/
I recently found a simple approach to use generated scripts for specific controller. I use for that solution gem gon. Add in a controller:
class HomesController < ApplicationController
before_filter :remember_controller
private
def remember_controller
gon.controller = params[:controller]
end
end
After that open your homes.js.cofee and add in the beginning of file:
jQuery ->
if gon.controller == "sermons"
# Place all functions here...
That is all.

Using Rails' stylesheet_link_tag and javascript_include_tag in a Liquid Drop

I want to be able to give the users full control and edit the layout. I also want them to be able to include what javascript plugins they want. Therefore, I had to make an interface to allow them to do that.
For example, the default html looks like a more complicated version of this:
<head>
<title>{{site.name}}</title>
...
{{js_plugins.colorbox}} # this should return the necessary javascript and/or stylesheet tags
</head>
My Liquid JsPlugins drop is like this:
class JsPluginsDrop < Liquid::Drop
include ActionView::Helpers::AssetTagHelper
...
def colorbox
javascript_include_tag "/path/to/js"
end
end
When I run my specs though, I get this error (note that you see #drop["colorbox-1.3.15"] when the code I supplied above acts differently. However, I wanted to simplify my code since that's not the problem, it's the usage of the TagHelper that is the problem):
Failures:
1) JsPluginsDrop colorbox-1.3.15 should return the correct script tags
Failure/Error: #drop["colorbox-1.3.15"].stylesheets.should include("/jquery-plugins/colorbox-1.3.15/example1/colorbox.css")
undefined local variable or method `config' for #<JsPluginsDrop:0xcbfab38>
# ./app/drops/js_plugins_drop.rb:22:in `stylesheets'
# ./spec/models/js_plugins_drop_spec.rb:11
I won't be surprised if the problem is caused by the fact that this is separate from my Rails environment, and the drop does not have access to the config of Rails. Since I still want to be able to use these convenience methods and :cache => true that they give, how can I use the stylesheet_link_tag and javascript_include_tag from within a drop, if it's possible at all?
It seems that this is possible now when done this way:
class MyDrop < Liquid::Drop
...
def my_js_tag
helpers.javascript_include_tag '/some/thing'
end
...
def helpers
#helpers ||= ActionController::Base.helpers
end
end

Change default stylesheet dir in rails

Would anyone know to change the default stylesheet directory /public/stylesheets to /public/css in rails 3?
I found a variable called
config.stylesheets_dir = '/css'
This didn't work though.
I know I can do <%= stylesheet_link_tag '/css/mystyle.css' %> but I'm curious if there's a better way.
Javascript and stylesheets paths were not fully dehardcoded in Rails 3.
To override these paths you need to monkey patch (with all consequences of that)
private method:
module ActionView::Helpers::AssetTagHelper
private
def compute_stylesheet_paths(*args)
expand_stylesheet_sources(*args).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) }
end
end
and additionaly this one if you use it:
def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css')
end
Alternatively, here's what I'm doing. I create a wrapper asset_tag that can be used like this:
<%= asset_tag 'mystyle', :css %>
<%= asset_tag 'mycode', :js %>
And then I define it in the application_helper:
module ApplicationHelper
# here is where you define your paths
# in this case, paths will be '/css/mystyle.css' and '/js/mycode.js'
def asset_path(asset, type)
return "/css/#{asset}.css" if type == :css
return "/js/#{asset}.js" if type == :js
end
def asset_tag(asset, type)
return stylesheet_link_tag asset_path(asset, type) if type == :css
return javascript_include_tag asset_path(asset, type) if type == :js
end
end
This way you can change the asset paths in any way you want and it will always be forwards compatible.

Resources