44

I'm using Webpack to build an Angular 1.4 project. The project makes use of several jQuery plugins, which are wrapped into angular directives. Those directives internally use angular.element, probably implying that angular.element is the real jQuery, not jqLite.

I want angular to auto-detect jQuery and use it instead of jqLite. I tried to require jquery locally in my entry point module app.js: require('jquery') and to expose jQuery globally with require(expose?$!expose?jQuery!jquery).

Still, whatever I do, angular.element refers to jqLite.


My research resulted in several findings:

  1. Even when imported as a CommonJS module, Angular assigns itself to a global variable window.angular, so I don't need to expose it with Webpack: Does Angular assign itself to `window.angular` globally, when loaded as CommonJS module?.
  2. ProviderPlugin doesn't seem to do the trick: it doesn't expose jQuery to global namespace; instead, for every module that depends on global name jQuery, it inserts require('jquery') in it. I'm not 100% sure, but looks like Angular doesn't access jQuery from global namespace directly, instead, it tries to access window.jQuery in bindJQuery function, so this approach doesn't help: Expose jQuery to real Window object with Webpack.
  3. For the same reason as ProviderPlugin, imports-loader seems unfit: Angular wants window.jQuery, not just jQuery.
  4. With expose-loader, jquery makes it to the window object. My problem was that Babel hoists all of its imports to the top of module in the resulting code. Hence, although require(expose?jquery!jquery) was before import angular from "angular" in source files, in bundle require("angular") is at the top of the file, before jquery, so by the time Angular is imported, jquery is not yet available. I wonder, how to use Webpack loaders with ECMA6 import syntax.
  5. There was a suggestion to use import syntax instead of require syntax with jquery: import "jquery" or import $ from "jquery", not require(jquery): (Petr Averyanov: How to use Webpack loaders syntax ( imports/exports/expose) with ECMAScript 6 imports?). jquery source code is wrapped with a special wrapper, which idenitifies how jquery is required (with AMD/require, CommonJS or globally with <script> statement). Based on that it sets a special argument noGlobal for jquery fabric and either creates window.jQuery or not, based on the value of noGlobal. As of jquery 2.2.4, upon import "jquery" noGlobal === true and window.jQuery is not created. IIRC, some older versions of jquery didn't recognize import as CommonJS import and added imported jquery to global namespace, which allowed angular to use it.

Details: here's my app.js:

'use strict';

require("expose?$!expose?jQuery!jquery");
require("metisMenu/dist/metisMenu");
require("expose?_!lodash");
require("expose?angular!angular");

import angular from "angular";
import "angular-animate";
import "angular-messages";
import "angular-resource";
import "angular-sanitize";
import "angular-ui-router";
import "bootstrap/dist/css/bootstrap.css";
import "font-awesome/css/font-awesome.css";
import "angular-bootstrap";

require("../assets/styles/style.scss");
require("../assets/fonts/pe-icon-7-stroke/css/pe-icon-7-stroke.css");

// Import all html files to put them in $templateCache
// If you need to use lazy loading, you will probably need
// to remove these two lines and explicitly require htmls
const templates = require.context(__dirname, true, /\.html$/);
templates.keys().forEach(templates);

import HomeModule from "home/home.module";
import UniverseDirectives from "../components/directives";

angular.module("Universe", [
    "ngAnimate",
    "ngMessages",
    "ngResource",
    "ngSanitize",
    "ui.router",
    "ui.bootstrap",

    HomeModule.name,
    UniverseDirectives.name,
])
.config(function($urlRouterProvider, $locationProvider, $stateProvider){
    // $urlRouterProvider.otherwise('/');

    // $locationProvider.html5Mode(true);

    $stateProvider
      .state('test', {
        url: "/test",
        template: "This is a test"
      });
});
Community
  • 1
  • 1
Boris Burkov
  • 13,420
  • 17
  • 74
  • 109

5 Answers5

52

Got this answer from john-reilly:
The mysterious case of webpack angular and jquery

bob-sponge's answer is not quite right - the Provide plugin is actually doing a text replacement on modules it processes, so we need to provide window.jQuery (which is what angular is looking for) and not just jQuery.

In your webpack.config.js you need to add the following entry to your plugins:

new webpack.ProvidePlugin({
    "window.jQuery": "jquery"
}),

This uses the webpack ProvidePlugin and, at the point of webpackification (© 2016 John Reilly) all references in the code to window.jQuery will be replaced with a reference to the webpack module that contains jQuery. So when you look at the bundled file you'll see that the code that checks the window object for jQuery has become this:

jQuery = isUndefined(jqName) ?
  __webpack_provided_window_dot_jQuery : // use jQuery (if present)
    !jqName ? undefined : // use jqLite
    window[jqName]; // use jQuery specified by `ngJq`

That's right; webpack is providing Angular with jQuery whilst still not placing a jQuery variable onto the window. Neat huh?

Community
  • 1
  • 1
studds
  • 1,395
  • 10
  • 15
9

!!Update

Apparently you still need to use the commonJs require for angular in the ES6 example.

import $ from "jquery"

window.$ = $;
window.jQuery = $;

var angular = require("angular");

below is the original answer



I want to purpose a easier solution. Just make jQuery a window global so that angular can recognize it:

var $ = require("jquery")

window.$ = $;
window.jQuery = $;

var angular = require("angular");

or in your case (OUT DATED):

import $ from "jquery"

window.$ = $;
window.jQuery = $;

import angular from "angular";

I hope this helps :)

Lukas Chen
  • 373
  • 7
  • 15
  • Your solution is good. Besides, I found out that you can just `import jquery; import angular` and angular should detect jquery (see update to the question, #5). Could you test, if that works? – Boris Burkov Jul 26 '16 at 12:19
  • Hm, I tested it myself. WIth jquery 2.2.4 noGlobal=true with `import $ from "jquery"` and `import "jquery"`. – Boris Burkov Jul 26 '16 at 14:38
  • And what is your result? – Lukas Chen Jul 27 '16 at 05:26
  • Ah, sorry, the result is negative - it doesn't work. `jquery`'s wrapper recognizes that it is imported with as a flavor of CommonJS, sets noGlobal to true and doesn't set window.$, thus angular doesn't detect it. – Boris Burkov Jul 27 '16 at 09:50
  • Can you use my solution with noGlobal=false. – Lukas Chen Jul 27 '16 at 10:00
  • If jquery were to set its noGlolal=false, it would've assigned window.$ = window.jQuery =$, essentially replicating your solution. I think, more webpack-ish solution is to use expose loader, as suggested in approach #4, as webpack does the same as you do by itself. – Boris Burkov Jul 27 '16 at 10:30
3

In your case is better to use ProvidePlugin. Just add this lines to your webpack config in plugins section and jquery will available in your app:

    new webpack.ProvidePlugin({
         "$": "jquery",
         "jQuery": "jquery"
    })
Bob Sponge
  • 4,708
  • 1
  • 23
  • 25
  • Hi, Bob Sponge! This didn't help: `angular.element` -> `function JQLite()`. Though, I don't understand, why angular was exported to the global namespace at all - I've commented out the line `require("expose?angular!angular");`. – Boris Burkov Mar 18 '16 at 08:57
  • In fact, I imported `angular` twice in the `app.js` - locally and globally - which might introduce confusion. I'd like to remove the local import but I can't. WIthout the local `import angular from "angular";` browser curses about `webpack:///./bower_components/angular-animate/angular-animate.js?:9 Uncaught TypeError: Cannot read property 'noop' of undefined`. The global `require("expose?angular!angular");` was done for debugging purposes and will be removed in production. – Boris Burkov Mar 18 '16 at 09:04
  • Be sure that you use jQuery 2.1+ and `window.jQuery` is not `undefined` – Bob Sponge Mar 18 '16 at 10:24
  • jQuery 2.2.1, yes it is availabe under names jQuery and $ (even without ProviderPlugin). – Boris Burkov Mar 18 '16 at 11:37
  • Actually I'm not using angular with webpack and can only suggest. So, try import jquery directly in angular: `var angular = require("imports?jQuery=jquery!angular")` – Bob Sponge Mar 18 '16 at 20:14
  • I did some additional research and updated the question with its results. – Boris Burkov Mar 21 '16 at 19:09
  • 4
    Finally solved it. It was due to Babel, putting import ahead of requires. It rearranged my code, so that angular was always imported before jquery was required. Damn. :) – Boris Burkov Mar 21 '16 at 20:41
  • Interesting observation, I didn't know that. Thanks for sharing results of your research. – Bob Sponge Mar 22 '16 at 06:05
  • @Bob: How did you resolve this issue when Babel was rearranging your code. I am facing the same issue and I guess your solution can work for me as well. – invincibleDudess Apr 15 '16 at 05:27
  • 1
    @invincibleDudess I didn't quite solve it - I just replaced all the ECMA6 `import`s with CommonJS `require`s, as there's no way in Webpack to say `require("expose?$!expose?jQuery!jquery");` in ECMA6 `import` syntax. Although, there's another solution. It turned out that `jquery`, `lodash` and `angular`, when `import`ed in ECMA6 just add themselves to the `window` object and become globally available, so for them you don't need `expose` loader. So you can just use ECMA6 `import`s everywhere, unless some of your minor libraries requires exposing. Just in case I sticked with `require` so far. – Boris Burkov Apr 15 '16 at 08:55
2

So, I give my solution, which is actually a mashup of the @BobSponge answer and @Bob's hints / comments. So nothing original, just showing what works for me (in a project not using Babel / ES6, BTW) and attempting to explain why it works...

The (final) trick is indeed to use the expose loader.

As explained in their page, we have to put in module.loaders:

{ test: require.resolve("jquery"), loader: "expose?$!expose?jQuery" },

Moreover, in the plugins list, I have:

        new webpack.ProvidePlugin(
        {
            $: 'jquery',
            jQuery: 'jquery',
            _: 'lodash',
            // [...] some other libraries that put themselves in the global scope.
            //angular: 'angular', // No, I prefer to require it everywhere explicitly.
        }),

which actually finds the global occurrences of these variables in the code, and require them into variables local to each module. It eases the migration of an existing project (from RequireJS to Webpack) as I do... I think we can do without this plugin if you prefer to be explicit in your imports.

And, importantly (I think), in the entry point of the application, I require them in the order I want them. In my case, I made a vendor bundle, so that's the order in this bundle.

require('jquery');

require('lodash');
// [...]

var angular = require('angular');
// Use the angular variable to declare the app module, etc.

Webpack will add the libraries to the relevant bundle in the order it sees the requires (unless you use a plugin / loader that reorder them). But the imports are isolated (no global leak), so Angular wasn't able to see the jQuery library. Hence the need for expose. (I tried window.jQuery = require('jquery'); instead, but it didn't work, perhaps it is too late.)

PhiLho
  • 40,535
  • 6
  • 96
  • 134
  • what do you mean by `in the vendor bundle, as I made one` ? I mean... where exactly do you put that code that defines the order? – refaelos Nov 16 '16 at 20:00
  • 2
    I had to re-read myself a couple time to understand what I meant... :-) I want these libraries to be ordered in the vendor bundle. The `require`s in the right order are put in the app bundle, ie. in the app code, of course. – PhiLho Nov 17 '16 at 13:54
2

There is this japanese article I want to use the jQuery not jQLite in webpack + AngularJS that seems to talk about the same problem (I don't know Japanese yet btw). I used google to translate to english, credits goes to cither for this nice answer.

He provides four ways to solve this:

  1. Assign directly to the window (not really cool)

    window.jQuery = require('jquery');
    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    
  2. Use the expose-loader (ok, but not that cool)

    npm install --saveDev expose-loader
    

    webpack.config.js

    module.exports = {
        entry: "./index",
        output: {
            path: __dirname,
            filename: "bundle.js"
        },
        module: {
            loaders: [{
                test: /\/jquery.js$/,
                loader: "expose?jQuery"
            }]
        }
    };
    

    usage:

    require('jquery');
    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    
  3. Use expose-loader (better)

    npm install --saveDev expose-loader
    

    webpack.config.js

        module.exports = {
        entry: "./index",
        output: {
            path: __dirname,
            filename: "bundle.js"
        },
        module: {
            loaders: [{
                test: /\/angular\.js$/,
                loader: "imports?jQuery=jquery"
            }, {
                test: /\/jquery.js$/,
                loader: "expose?jQuery"
            }]
        }
    };
    

    usage:

    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    
  4. Use ProvidePlugin (Best solution)

    This is actually the same as studds's accepted answer here

    module.exports = {
        entry: "./index",
        output: {
            path: __dirname,
            filename: "bundle.js"
        },
        plugins: [
            new webpack.ProvidePlugin({
                "window.jQuery": "jquery"
            })
        ],
    };
    

    usage:

    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    

I thought I'd share this here since we had the exact same problem. We used the expose-loader solution in our project with success. I suppose that the ProvidePlugin which injects jquery directly in window is also a good idea.

Community
  • 1
  • 1
GabLeRoux
  • 16,715
  • 16
  • 63
  • 81