Rails 6 webpacker $(...).modal is not a function - importing dynamic packs - javascript

I have a Rails 6 app in which I am trying to use webpacker. I was using it succcessfully and importing all packs in my application.js file but instead now I just want to import the application.js file that has jquery and bootstrap in it, and dynamically load the correct pack based on the controller.
For instance, my previous configuration was
import "bootstrap"
require("#rails/ujs").start()
require("turbolinks").start()
require("#rails/activestorage").start()
require("channels")
require("jquery")
require("../packs/classrooms")
require("../packs/lunch_choices")
require("../packs/events")
require("../packs/users")
What I'd rather do is remove all packs from the application.js file and just import the correct pack dynamically like so...
Application.html.erb
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag "#{controller_name}", 'data-turbolinks-track': 'reload' %>
If I view the source code when I reload the page and restart the server, I see that the pack does load, however, I get the following error.
$(...).modal is not a function
Since the application file is being loaded first and that's the one that contains jquery, why am I getting this error? This was working when I was including all packs in application.js, but now that I want to split by the pack it does not.
Here is my environments.js file - this has never changed since setting up my app.
const { environment } = require('#rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
jquery: 'jquery',
Popper: ['popper.js', 'default']
})
)
module.exports = environment

Each pack is a separate dependency graph. Without extra configuration, the graphs are not shared. I would guess you’re either accessing a function on the wrong dependency graph or possibly clobbering one instance of jQuery for another in the global scope.
Splitting your code into multiple packs, while possible, is error-prone and works against the way webpack was intended to be used.
Try consolidating everything into just one pack and splitting code with dynamic imports instead. Your “page-specific” code would live in another folder, like app/javascript/pages, and might be imported conditionally, based on the presence of a certain DOM element, for example. This achieves the goal of producing smaller bundles from one pack while keeping all your code in the same dependency graph.
https://webpack.js.org/guides/code-splitting/

Related

Why are my js.erb views not working when using webpacker in Rails 6 with bootstrap?

Im trying to add som simple Ajax to my rails app. I am using Bootstrap with webpack.
My webpack/environment.js file looks like this
const { environment } = require('#rails/webpacker')
const webpack = require('webpack')
environment.plugins.append('Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Popper: ['popper.js', 'default']
})
)
module.exports = environment
My javascript/packs/application.js looks like this
require("#rails/ujs").start()
require("turbolinks").start()
require("#rails/activestorage").start()
require("channels")
import 'bootstrap'
import 'src/main'
import 'style/main'
I'm trying to add some Ajax for one of my models in create.js.erb
$("#question-<%= #exam_option.exam_question.id %>-options").append("<%= escape_javascript render 'exam_option', option: #exam_option %>");
$('#add_option_modal').modal('hide');
When I try and add one of my options, I get the console error
ReferenceError: Can't find variable: $
I've searched for a solution and have been unsuccessful.
What am I doing wrong? Thank you!
Edit:
When I added Bootstrap to my app, I followed this guide . When following the guide, I installed bootstrap, jquery and popper.js with yarn
yarn add bootstrap jquery popper.js
The jQuery that Bootstrap uses is working correctly (such as tooltips).
as per #mechnicov's answer, I tried changing my environment.js to
const webpack = require('webpack')
environment.plugins.append('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery',
Popper: ['popper.js', 'default']
})
)
and I added require("jquery") above import 'bootstrap' in my application.js
when I made those changes, my code in create.js.erb works correctly, but it makes my Bootstrap not function correctly and throw errors such as TypeError: ... $('[data-toggle="tooltip"]').tooltip() undefined.
Edit 2
As per the accepted answer, I changed my application.js to :
require("#rails/ujs").start()
require("turbolinks").start()
require("#rails/activestorage").start()
require("channels")
window.jQuery = window.$ = require('jquery')
import 'bootstrap'
import 'src/main'
import 'style/main'
All seems to be working correctly, but I'm unsure why I need to add it this way? isn't Webpack supposed to do this from my environment.js file?
If someone can explain this, please do.
Can you try below code :-
# javascript/packs/application.js
window.jQuery = window.$ = require('jquery')
OR
# javascript/packs/application.js
require("jquery")
window.jQuery = $;
window.$ = $;
I was also facing same problem but above code was worked for me at that time
Hope this will help you also. :)
All seems to be working correctly, but I'm unsure why I need to add it
this way? isn't Webpack supposed to do this from my environment.js
file? If someone can explain this, please do.
I'm going to answer your question based on the edit you have done in your question, what you have to understand is how webpack works, basically you have:
1 - an entry file which is your source code file (that contains "imports" and/or other javascript code)
2 - Webpack takes that entry file, bundle it, and produces an output file. In your application, this is what you end up using, the final output/bundle.
In Rails, Webpacker is the gem that configures webpack for you out of the box and hides the boring details from you so you can just jump and start using it.
In term of webpacker any file you put in javascript/packs is considered as an entry file, by default we have application.js there, so app/javascript/packs/application.js is your entry file, the output file will have the same name with a hash string attached to it, something like this application-ff14101dff18182f89f6.js
Let's imagine you add another file to app/javascript/packs called admin.js (which is another entry file), what will happen is that Webpacker is configured to make an output file from each entry file. That means after compilation, you will have another file admin-ff14101dff18182f89f6.js as output.
The output files are placed under /packs/js in your Rails application, and you can call them from layout as follows:
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'admin', 'data-turbolinks-track': 'reload' %
You can even use the first tag in your layouts/application.html.erb and the second one in another layout, for example, layouts/admin.html.erb, you don't have to push all your code in one giant application.js (yep you can make your site faster by loading only the necessary scripts).
Now that you understand the basics, it's time to answer your question as we said before Webpacker is using any JS file in app/javascript/packs as an entry file, you can verify this by opening config.webpacker.yml file, and locate the following:
default: &default
source_path: app/javascript
source_entry_path: packs // <------ The location of the entry files
public_root_path: public
public_output_path: packs // <------ The location of the output/bundle
...
So even when you are exposing jQuery in environment.js via the ProvidePlugin, it's meant to be available for the webpack configured source path which is app/javascript only. That explains why you cannot access jquery from app/views/some-view-folder/create.js.erb.
You may ask, can expose the view folder app/views/some-view-folder to webpack somehow? Theoretically yes, by "resolving" that folder which webpacker has a configuration for it (see config/webpacker.yml):
# Additional paths webpack should lookup modules
# ['app/assets', 'engine/foo/app/assets']
resolved_paths: []
It's possible to do something like resolved_paths: ['app/views/some-view-folder'] which means if you import create.js.erb in your application.js webpack will be able to find that file and import it, however practically it doesn't make any sense as create.js.erb needs to be executed at run time and execute other ruby code from your application and it's used to render a view dynamically not for a static purpose like a JS file does (sorry I don't know much about Rails internal but you got the idea)
By doing window.jQuery = window.$ = $ you are exposing jQuery to the window object, which makes it a global, but you can always use a regular ajax request in a file under app/javascript instead of relying on something like remote: true depending on if you want to expose things globally or not.
I hope this was somehow useful, I have written a book for beginners about webpack which you can check on amazon or on apress if that interests you, that will give you a full understanding how webpack works whether you are working with Rails or any other framework :)
You don't load JQuery.
Add it to your node modules:
$ yarn add jquery
Write right path to JQuery in config/webpack/environment.js
const { environment } = require('#rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery',
Popper: ['popper.js', 'default']
})
)
module.exports = environment
Add line before bootstrap in javascript/packs/application.js:
require("jquery")
And don't forget about javascript_pack_tag 'application' in main layout view.
Hope this will help you.

How to properly import JS and CSS files in Webpacker?

I installed Webpacker gem into my Rails app and have difficulties to import JS and CSS files from assets folder to app/javascript/packs folder.
I'm mentioning that app/javascript/packs/application.js looks like this:
/* eslint no-console:0 */
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
//
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
console.log('Hello World from Webpacker')
and it works properly(in console I get this "Hello World.." text.)
I tried to copy, for example jquery.slimmenu.js into packs folder and to include it like this:
//= require jquery.slimmenu.js
but it's not finding in console, there is just application.js and bunch of 404 not find JS and CSS files
your main.js file can be in any directory in app/javascript
for example, create app/javascript/components
then add in your application.js
import 'jquery'
require("../components/main");
then in your HTML add
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
and script from application.js must be loaded

Rails .js.erb templates no longer work with Webpack

I've just switched my Rails app over to use WebPack to deal with assets etc. It's working well apart from I have some JS templates in my views directory (*.js.erb) . These require jQuery and as jQuery is pulled in as part of my WebPack bundles, it is not working in those templates.
Is there a way to allow those templates to work?
I got this to work in my app by adding the expose-loader package, then adding these two lines to my app/javascript/packs/application.js:
import 'expose-loader?$!jquery';
import 'expose-loader?jQuery!jquery';
Well in order to make things work you need to include jquery using yarn which works with the latest version of rails.
In Rails 5.1 this is done with the new JavaScript packet manager Yarn which you have to install first
sudo apt-get install yarn
Then you can use it to install jQuery:
yarn add jquery
To actually load jquery you have to add this line
//= require rails-ujs
//= require jquery
//= require turbolinks
//= require_tree .
After installing jquery your js.erb file will start working
Refer this article
This is how I use jquery in .js.erb when I use webpack and without asset pipeline.
(I assume that the Rails is created with something like $ rails new MyApp --webpack)
First of, I replace <%= javascript_include_tag 'application' %> with <%= javascript_pack_tag 'application' %> so we use javascript from /app/javascript/pack/application.js instead of /app/assets/javascripts/application.js
Add rails-ujs and jquery by running
$ yarn add rails-ujs jquery
modify /config/webpack/environment.js as following
const { environment } = require('#rails/webpacker')
const webpack = require('webpack')
// Add an additional plugin of your choosing : ProvidePlugin
environment.plugins.prepend(
'Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jquery: 'jquery/src/jquery'
})
)
module.exports = environment
The webpack configuration is reference from here
I make a test page like this...
in /app/views/pages/index.html.erb
<div class="index">
<h1 class="index--title">Index Title</h1>
<div class="index--jquery-test">
<%= link_to "jquery test", pages_index_path, remote: true %>
</div>
</div>
and in /app/views/pages/index.js.erb it has following code, that will run jquery when the link is clicked
$(function(){
console.log('run <%= "JQuery" %> from .js.erb')
})
In /app/javascript/pack/application.js import Rails and call Rails.start() to allow the date-remote in the link to work and to make unobtrusive javascript to call index.js.erb file.
I also try to see if jquery from within pack/application.js file is also work by calling console.log to show a title text that I got from jquery selector $('.index--title').text()
import Rails from 'rails-ujs'
Rails.start()
$(function() {
console.log($('.index--title').text())
})
When run Rails app at, let say, http://localhost:3000/pages/index you should see the 'Index Title' in console windows of the browser. And when click the link you should see 'run JQuery from .js.erb' in the console windows. I hope it works for you.

How to import javascript files from the same directory as a Rails view

How can I precompile and import a javascript file, while keeping it in the same folder as the views its associated with?
For example, I'd like to keep companies.js inside the same /view directory as my other company views
For example:
/app/views/companies/_form.html.erb
/app/views/companies/index.html.erb
/app/views/companies/new.html.erb
/app/views/companies/edit.html.erb
/app/views/companies/companies.js <--- like this
This allows better organization than a large number of javascript files in /app/assets/javascripts/.
I've seen it done before, but I've been unable to replicate it now.
Using <% javascript_include_tag 'companies' %> tries to find the file in /assets/javascripts/.
Try change the manifest file in app/assets/config/manifest.js to compile .js in view's folders.
E.g
//= link_directory ../javascripts .js
add this with the path to your view folder

Rails production asset compress and integration

Fast Example,
lets say, i have this js file, test.coffee
alert 'test!'
my goal is,
I dont want this code to be loaded in every pages
so i manually included where i want,
<%= javascript_include_tag 'test'%>
but the tragedy happens in production mode,
this test.coffee is not minified in production mode!
I want this code to be minified, but it should not integrate and minified to application.js, because i don't want this code be loaded in every pages.
How can i solve this dilemma?
You are having this issue because rails by default doesn't precompile file with the coffee extension. You should be able to solve this issue be prepending the .js extension so your file should look like this:
test.js.coffee
Straight from the rails docs:
When using asset precompilation, you will need to ensure that your controller assets will be precompiled when loading them on a per page basis. By default .coffee and .scss files will not be precompiled on their own. See Precompiling Assets for more information on how precompiling works.
Note:
There's a mechanism in rails that allows you to "inject" javascript at runtime.
Typically you would define a yield :javascripts in your app layout.
And add content to this yield using:
<% content_for :javascripts do %>
<%= script_tag :test %> # Content here will be yielded
<% end %>
This allows you to define your javascript at the bottom page while injecting page specific assets.

Categories

Resources