Boris Burkov
Boris Burkov

Reputation: 14476

Webpack: how to make angular auto-detect jQuery and use it as angular.element instead of jqLite?

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"
      });
});

Upvotes: 44

Views: 14662

Answers (5)

PhiLho
PhiLho

Reputation: 41152

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.)

Upvotes: 2

GabLeRoux
GabLeRoux

Reputation: 17933

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.

Upvotes: 2

studds
studds

Reputation: 1405

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?

Upvotes: 52

Lukas Chen
Lukas Chen

Reputation: 383

!!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 :)

Upvotes: 9

Bob  Sponge
Bob Sponge

Reputation: 4738

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"
    })

Upvotes: 3

Related Questions