Webpacker - Bootstrap - JQuery, what am I doing wrong (and why so hard?)

I have a Rails 6 app that uses jQuery and Bootstrap - including js elements from Bootstrap.

this code should show the modal, but only works in some places:

$("#modalOverlay").modal("show");

    • run from console: works
    • code returned from remote render (ujs): works
    • code in included player.js file: error
player.js:126 Uncaught TypeError: $(...).modal is not a function
    at Object../app/javascript/custom/player.js.hs.showModal (player.js:126)
    at <anonymous>:1:4

I’m particularly frustrated because this seems like a simple thing that should just work, but it has become some kind of dark magic that I don’t understand.

Here is my setup (which I have cobbled together from internet comments and tutorials)

bootstrap & jQuery were both installed via yarn

#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',
    noUiSlider: 'nouislider',
    Popper: ['popper.js','default'],
  })
)

module.exports = environment
#app/javascript/packs/application.js

require("@rails/ujs").start()
require("@rails/activestorage").start()
require("channels")


//copied from https://github.com/jasl/cybros_core/blob/master/app/javascript/packs/application.js
//via https://gorails.com/forum/how-to-use-bootstrap-with-webpack-rails-discussion
import JQuery from 'jquery';
window.$ = window.JQuery = JQuery;

import 'bootstrap'


require("nouislider")
require("custom/player.js")


// Uncomment to copy all static images under ../images to the output folder and reference
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
// or the `imagePath` JavaScript helper below.
//
// const images = require.context('../images', true)
// const imagePath = (name) => images(name, true)



// Styles
// These are imported separately for faster Webpack recompilation
// https://rubyyagi.com/solve-slow-webpack-compilation/
import "stylesheets/nouislider.scss"

and then the code which fails is

#app/javascript/custom/player.js

...

hs.showModal = function() {
    $("#modalOverlay").modal("show");
}

..

note that this is included via application.js

calling hs.showModal() from the console (or running it any other way) gives the error

Uncaught TypeError: $(...).modal is not a function
    at Object../app/javascript/custom/player.js.hs.showModal (player.js:126)
    at <anonymous>:1:4

can anyone explain what is going on and how I should fix it?

many thanks.

2 Likes

so - still dark magic, but by copying from Chris Oliver’s webcast, I now have a working setup.

I’ll put my details below.

I would love to see someone adding a definitive rails tutorial that actually explains what is going on here though. What is webpack/environment doing, what is javascript/application.js doing

how do they interact, when and why does something get added to one or the other

as far as I can tell, I had two errors:

  1. not assigning jQuery to window.$ before requiring ujs
  2. pointing to the wrong path in environment.js

#webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack');

environment.plugins.append('Provide',
	new webpack.ProvidePlugin({
		$: 'jquery',
		jQuery: 'jquery',
		Popper: ['popper.js', 'default'],
    	noUiSlider: 'nouislider',
}))

module.exports = environment
#app/javascript/packs/application.js
//copied from https://github.com/jasl/cybros_core/blob/master/app/javascript/packs/application.js
//via https://gorails.com/forum/how-to-use-bootstrap-with-webpack-rails-discussion
import JQuery from 'jquery';
window.$ = window.JQuery = JQuery;

require("@rails/ujs").start()
require("@rails/activestorage").start()
require("channels")

import 'bootstrap'


require("nouislider")
require("custom/player.js")


// Styles
// These are imported separately for faster Webpack recompilation
// https://rubyyagi.com/solve-slow-webpack-compilation/
import "../stylesheets/application.scss"

thanks.

1 Like

That would be great. Some of the smarter people wrote blogs over the past couple of years and they don’t get consolidated and updated. the Rails documentation is way behind. And I suspect many are assuming that a better solution will come along.

Thanks for posting your solution. I went through some of this. About a year ago I picked up a project that hadn’t been updated and thought, well webpacker looks like the future, but I didn’t know enough to handle all of this. I did get my app working after many SO posts and reading. One trivial change I made that made it easier for me was to rename the javascript folder webpacker. One config reference needs to be changed for this to work. But it separates the old javascript folder from the new and relocates next to views if you use an editor that shows the structure. Of course I wasn’t able to make this change until I understood better how it worked when it would have helped early. None of this falls into the category of “it just works.”

guides.rubyonrails.org still has Webpacker write up in Edge Rails as it seems Webpacker is a bit more main stream now.

I suspect that you were bundling two instances of jQuery. The $.fn.modal was attaching to a different instance than the one you were trying to access.

What ProvidePlugin does is shim modules with imports automatically. When you specify jquery: "jquery/src/jquery" as an option, you’re saying: "add import jQuery from 'jquery/src/jquery' everywhere jQuery is referenced in these other modules I’m bringing into my app, including bootstrap! That means webpack can rewrite the source code of imported modules, including 3rd parties, in your build. (I think it’s kind of cool that webpack can do this sort of thing, but I can see why it comes across as dark magic).

Your own import 'jquery' line is pointing to a different file: you can find the path listed in the main entry in jquery’s package.json file:

main: "dist/jquery.js"

This file is packaged for use directly in browsers. It obviously can work in webpack, as you’ve demonstrated, but it includes a lot of “module-ization” stuff (more offically, a Universal Module Definition) that webpack can already handle for you. The file jquery/src/jquery is structured as an ES module and is more suitable for use in webpack—that’s why tutorials probably pointed you to used it. When you try to import both though, webpack is likely treating these imports as two different modules in your runtime.

Changing the ProvidePlugin to jQuery: 'jquery'and $: 'jquery' fixed the issue because now the “auto” imports are pointing to the same module as your own import jQuery from 'jquery'.

Another way to fix the issue (again, I suspect) is you could have changed your import to match: import jQuery from 'jquery/src/jquery' within your application.js file.

Anyhow, I’m speculating since I don’t have access to your source, but I think that’s what’s going on and, that it’s likely moving the import prior to @rails/ujs had no effect (but I could be missing something). Hope that’s helpful!

2 Likes

I really like this idea. What was the config change you had to make? I might send a rails pull request with this…

Thanks for the response @rossta . I think you’re right about importing two jQueries.

the problem for me is that there is just so much going on beneath the surface that I don’t understand. I broadly understand the concept of modules (~namespaces), but I have no idea how they work in this context. So when you say:

I’m kinda baffled. I suspect I’m not the only casual rails dev with ‘good enough’ js skills to do a bit of js in an app, but nowhere near enough to follow what is happening with:

I’d love to see an ELI5 introduction to what is going on here!

I still have outstanding questions:

  1. how do I know what to put in environment.js jquery, popper and noUiSlider all have different forms

environment.plugins.append('Provide',
	new webpack.ProvidePlugin({
		$: 'jquery',
		jQuery: 'jquery',
		Popper: ['popper.js', 'default'],
    	noUiSlider: 'nouislider',
}))
  1. import vs require - does it matter? when would I use each

  2. assets/javascripts is this just legacy - or are there times I would put js here?

thank you

I knew at the time it was lame not to put that in, but I wasn’t at the right computer. Here’s the change:

# config/webpacker.yml

default: &default
  source_path: app/webpack

I also had to have import 'channels' in application.js even though I didn’t use channels.

controllers is for Stimulus which I was experimenting with in this app, but not required. I did have a problem with not All stylesheets moved to the folder shown below.

image

Thank you Ross for you explanation of what the jQuery imports are doing. With your help I’d gotten it working but didn’t understand it. This may get me closer to understanding.

Don’t put anything else in there (meaning the ProvidePlugin config) unless you need it for the same reasons it’s commonly recommended for jQuery: to make legacy JavaScript compatible with webpack. If you’re starting a new app, I’d recommend looking for modern libraries instead, i.e. you don’t need jQuery.

Use import.

It’ll be future-friendly and is already compatible with all modern browsers. Require is for CommonJS-style programming which Node.js uses. It won’t be natively supported in any browsers; webpack just makes it so.

1 Like

It’s for Sprockets-based assets as I’m sure you know. It’s still supported alongside Webpacker. Say you want to integrate with a gem that only supports the asset pipeline is one reason you might use it.

Thanks for those responses @rossta.

I had completely the wrong end of the stick regarding environment.js - I had understood it as the starting point for including things.

1 Like

@Confused_Vorlon Glad to be helpful.

Just to clarify: it’s okay to add stuff to environment.js—it is sometimes necessary to extend the default webpack config provided by Webpacker, e.g. to integrate Vue.js. My earlier comment (I should have been more specific) was about ProvidePlugin part—it’s not necessary to add every new library to that list.

Hi @Confused_Vorlon I see that @rossta and @MtnBiker ere able to help.

For what it’s worth I did the migration to Webpacker about an year ago and it was one of the hardest thing I’ve ever done in the Rails world and there are still a lot of teams going through this. Have you manage to successfully resolve it?

Thanks,

Yup - I got things working.

I’m starting to get a better handle on what is going on - but I think fundamentally the issue is that webpacker asks me to understand a lot about how js/packaging actually works under the hood with regard to modules, etc. My js just isn’t at that level.

This compares unfavourably (in my mind) to the way that bundler just tells me to add what I want as a line in my gemfile and then (mostly) not have to think about it or understand what is happening.

True true. I’ve seen a number of issue where people just ask for it to be “the rails way” and to just work. I think the webpacker team has done a good job in trying to make a lot of the things that are going on a little easier, but fundamentally it is JS - the whole community thinks in a different way.

This has helped me a lot - Webpack from Nothing - What Problem Are We Solving? It takes an hour but I like the author style and is one of the better resources about webpack.

1 Like

Thanks for the link. I skimmed through rather than reading properly. I’m still left with the overwhelming sense that it doesn’t have to be so painful.

As far as I can see, we’re talking about namespaces and imports. Some sensible defaults and a list of packages (like a gemfile) seem to contain all the required information…

Perhaps it is useful that I call import jquery as window.RobHasASpecialJQuery, but that could certainly be an option which defaults to $…

This is how I do it

// NOTE jquery
// this exposes jquery to be available in the views
// <script>
//   console.log($('#tree'))
// </script>
environment.loaders.append('expose', {
  test: require.resolve('jquery'),
  use: [{
    loader: 'expose-loader',
    options: '$'
  }, {
    loader: 'expose-loader',
    options: 'jQuery',
  }]
})
// END NOTE

which I use for views that are still sprockets dependent.

compare:

gem 'jquery-rails'

and an imagined equivalent

rails_webpack 'jquery'

It’s nuts that you have anything useful to show!

edit: updating to note that I’m being slightly unfair.

The gemfile way also requires adding

//= require jquery

to app/assets/javascripts/application.js

of course in my imagined rails_webpack world, that second step wouldn’t be required :slight_smile:

I guess we were just 3-4 years late. If we had shown the JS world in the beginning of last decade how good we have it with gems, probably they could have used… gems. It’s naive I know, but just imagine the possibilities.

I have a lot of useful this to show. I have platforms that are using sprockets and webpacker at the same time with other that even use teaspoons for tests and part of the logic comes from sprockets compiled js and another part from webpack. At the end of the day it is double.

I believe the problem is that when Rails decided to make the switch, webpack was the most mature frontend bundler and the only sensible choice. The field has changed considerably since then, and nowadays I would much rather use Parcel or Snowpack, which are both a lot closer to the Rails way with less config and sensible defaults. There are gems available for both of these, but they’re both in alpha, so not sure I would recommend using any of them in production. Snowpacker looks very nice, though, I’m tempted to test it on a real project.

1 Like