javascriptjqueryruby-on-railswebpackerslim-lang

JQuery not present in inline scripts after switching to Webpacker


I'm switching a Ruby on Rails application from sprockets to webpacker. I'm getting an error for inline javascript in my existing slim templates, JQuery is not defined. I tried adding a require for jquery in the application.html.slim but that doesn't help.

application.html.slim

doctype html
html
  head
    title My Application
    meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"
    meta name="have-i-been-pwned-verification" value="..." /

    meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"
    = include_gon(watch: true)
    = stylesheet_link_tag 'application', media: 'all'
    = javascript_pack_tag 'application'
    = javascript_include_tag 'jquery'
    = csrf_meta_tags
body
  = render 'layouts/navigation/main_nav', disable_search: true
  = flash_messages
  main.application
    = yield

error

Uncaught ReferenceError: $ is not defined

_comments.slim

.comments-wrapper.collapse.show.comments-collapse#comments
  .row.comments
    .col-sm-12
      .row.comment-post
        .col-sm-12.parent-comment-form
          - if user_signed_in?
            = render 'we_vote/comments/form', commentable: commentable
      - unless local_assigns[:limit_comments].present?
        .row.filter-row
          .col-sm-12.col-lg-2.offset-md-1.offset-sm-0.text-right
            .sort-label sort by
          .col-lg-4.col-sm-12.mb-2
            select.sort-select
              option Popular
              option Reply Number
              option Upvote Number
              option Newest
          .col-lg-4.offset-lg-1.col-sm-12.offset-md-0
            select.sort-select
              option Today
              option Last Week
              option This Week
      .row
        .col-sm-12.comments-container class="#{'empty' unless commentable.comments.any?}"
          ul.comments-list
            - if local_assigns[:limit_comments]
              - commentable.comments.top_level.by_popularity.limit(limit_comments).each do |comment|
                = render 'we_vote/comments/comment', comment: comment, commentable: commentable, limited: true
            - else
              - commentable.comments.top_level.by_popularity.each do |comment|
                = render 'we_vote/comments/comment', comment: comment, commentable: commentable

= render 'we_vote/comments/report_comment_modal'

javascript:

  $(function(){
    var commentId = "#{ params[:comment_id] }";
    if(commentId) {
      var $comment = $('#' + commentId);
      $comment.get(0).scrollIntoView();
      $comment.addClass('notified');
    }
  });

environment.js

const { environment } = require("@rails/webpacker");
const erb = require("./loaders/erb");
const jquery = require("./plugins/jquery");
const webpack = require("webpack");
environment.plugins.append(
  "Provide",
  new webpack.ProvidePlugin({
    "window.Tether": "tether",
    Popper: ["popper.js", "default"],
  })
);

const aliasConfig = {
  jquery: "jquery-ui-dist/external/jquery/jquery.js",
  "jquery-ui": "jquery-ui-dist/jquery-ui.js",
};

environment.config.set("resolve.alias", aliasConfig);

environment.plugins.prepend("jquery", jquery);
environment.loaders.prepend("erb", erb);
module.exports = environment;

config/webpack/plugins/jquery.js

const webpack = require("webpack");

module.exports = new webpack.ProvidePlugin({
  $: "jquery",
  jQuery: "jquery",
  "window.jQuery": "jquery",
});

application.js

import $ from "jquery";
import autosize from "autosize";
global.$ = $;
global.jQuery = $;
import tether from "tether";
global.Tether = tether;

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

require("../src/google_analytics");
require("jquery");
require("jquery-ui");
require("../src/autocomplete-rails");
require("tether");
require("popper.js/dist/umd/popper");
require("bootstrap/dist/js/bootstrap");
require("bootstrap-notify/bootstrap-notify");
require("jquery-mask-plugin");
require("select2/dist/js/select2.full.min");
require("jquery-textcomplete");
require("jquery-match-height");
require("jquery-jscroll");
require("../src/facebook");

require("../src/components/comment");
require("../src/components/discussion");
require("../src/components/follow");
require("../src/components/hashtaggable");
require("../src/components/news_feed");
require("../src/components/preview-img");
require("../src/components/question");
require("../src/components/remote_buttons");
require("../src/components/report");
require("../src/components/search");
require("../src/components/sidebar");
require("../src/components/upvote");
require("../src/components/vote");
require("../src/components/verification");

jQuery.railsAutocomplete.options.delay = 300;
jQuery.railsAutocomplete.options.autoFocus = true;

Solution

  • The problem is that your inline script gets executed before jquery. The reason for that, is because it's inlined in the html, so it's available right away. On the other hand, jquery is not inline so the browser has to make a request before executing it.

    There are several options.

    Option: wait for jQuery to be loaded

    One option is to wrap all you inline code in a function that waits for jQuery to be loaded.

    javascript:
      document.addEventListener('DOMContentLoaded', function () {
        var commentId = "#{ params[:comment_id] }";
        if(commentId) {
          var comment = document.getElementById(commentId);
          commment.classList.add('notified');
        }
      }, false);
    

    Option: stub jQuery

    Another option would be to create a stub (fake) $ function, store all the calls to $, wait for jQuery to be loaded and replay those calls. More details here