Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RequireJS effecting an embedded jquery-ui widget

I have a jQuery widget that are partner is trying to embed. The problem we are getting is the partner is using requireJS and its effecting the widget.

The widget is in an anonymous function and requires jquery-ui within. After debugging we have found that jQuery UI is being removed after the noConflict call. Here is the code from the widget.

(function () {

    // Localize jQuery variable
    var jQueryWidget;

    /******** Load jQuery if not present *********/
    if (window.jQuery === undefined || window.jQuery.fn.jquery !== '3.2.1') {
        var script_tag = document.createElement('script');
        script_tag.setAttribute("type", "text/javascript");
        script_tag.setAttribute("src", "https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js");
        script_tag.onload = scriptLoadHandler;
        script_tag.onreadystatechange = function () { // Same thing but for IE
            if (this.readyState == 'complete' || this.readyState == 'loaded') {
                scriptLoadHandler();
            }
        };
        (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
    } else {
        loadJQueryUi();
    }

    function scriptLoadHandler() {
        loadJQueryUi();    
    }

    function loadJQueryUi() {
    /******* Load UI *******/
        jQuery.getScript('https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js', function () {
          jQueryWidget = jQuery.noConflict(true);
          setHandlers(jQueryWidget);
        });


        /******* Load UI CSS *******/
        var css_link = jQuery("<link>", {
            rel: "stylesheet",
            type: "text/css",
            href: "https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css"
        });
        css_link.appendTo('head');
    }

    function setHandlers($) {
        $(document).on('focus', '#start-date, #end-date', function(){

      $('#start-date').datepicker({
        dateFormat: "M dd, yy",
        minDate: 'D',
        numberOfMonths: 1,
      });

            $('#end-date').datepicker({
                dateFormat: "M dd, yy",
                minDate:'+1D',
                numberOfMonths:1,
            });
    }
})();

Using chrome debugger we can see that when the getScript is called it correctly adds jquery-ui to the loaded version. Its straight after we call the noConflict that it restores the previous jQuery but are version no longer has jQueryUI.

Testing the widget on others sites without requireJS works correctly.

Has anyone came across this before? Unfortunately we have not worked with RequireJS before but cant see why it would effect are anonymous function.

Any help would be really appreciated.

like image 936
Lee Avatar asked Nov 08 '22 15:11

Lee


1 Answers

The problem is that what you are trying to do is unsafe. There are two factors that, combined, work against you:

  1. Scripts are loaded asynchronously. The only thing you control is the relative order in which your widget loads jQuery and jQueryUI. However, the page in which your widget operates also load its own version of jQuery. Your code cannot coerce the order in which scripts loaded by the partner code are going to load.

  2. jQuery is not a well-behaved AMD module. A well-behaved AMD module calls define to gets its dependencies and it does not leak anything into the global space. Unfortunately, jQuery does leak $ and jQuery into the global space.

With these two factors combined, you are facing a race condition depending on which order the two versions of jQuery are loaded: it is generally impossible to know which version of jQuery the global symbols $ and jQuery are referring to. Consider your code:

jQuery.getScript('https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js', function () {
  jQueryWidget = jQuery.noConflict(true);
  setHandlers(jQueryWidget);
});

You cannot know whether jQuery refers the version you asked be loaded or to the version that the partner code wanted to load. The only thing .getScript guarantees is that the callback will be called after the script is loaded, but it does not prevent other scripts from loading between the script that .getScript loads and the time the callback is called. The browser is absolutely free to load the script you give to .getScript, load some other script that was requested through RequireJS, and then call your callback.

If you want your widget to be something that can be plopped into a page without having to change any of the existing code, then there's no simple fix. You cannot just change the logic you show in your question, nor can you just add RequireJS to your widget. RequireJS cannot by itself fix this. Contrarily to what the other answer suggest, the context configuration option for RequireJS is not a fix. It would be a fix if there were no scripts that try to access jQuery through the global $ or jQuery, but there are a dozens of plugins for jQuery that do just that. You cannot ensure that the partner code does not use them.

And beware of proposed fixes that seem to fix the problem. You can try a fix, and it seems to work, and you think the problem is solved but really the problem is not manifesting itself because, well, it is a race condition. Everything is fine, until one month later, another partner loads your widget and boom: their page creates just the right conditions to cause things to load in an order that screws up your code.

There is an additional complication which you may not have run into yet but is bound to happen from time to time. (Again, you are dealing with race conditions, so...) You code is loading jQuery and jQuery UI through script elements. However, they both check whether define is available, and if so, they will call define. This can cause a variety of problems depending on the order in which everything happens, but one possible issue is that if RequireJS is present before your widget loads, jQuery UI will call define from a script element and this will give rise to a mismatched anonymous define error. (There's a different issue with jQuery, which is more complicated, and not worth getting into.)

The only way I can see to get your widget to load without interference from the partner code, and without requiring the partner to change their code would be to use something like Webpack to build your code into a single bundle in which define should be forced to be falsy in your bundle so that any code that tests for the presence of define is not triggered. (See import-loader, which can be used for this.) If you load your code as a single bundle, then it can initialize itself in a synchronous manner, and you can be sure that $ and jQuery refer to the jQuery you've included in your bundle.


If you are going to follow my advice here is a nice example, that takes full advantage of Webpack, includes correct minification, and eliminates some artifacts from your code that are no longer needed with this approach (for instance the IIFE, and some of the functions you had). It is runnable locally by saving the files, running:

  1. npm install webpack jquery jquery-ui imports-loader lite-server
  2. ./node_modules/.bin/webpack
  3. ./node_modules/.bin/lite-server

And there's something I did not realize when I first wrote my explanation but that I noticed now. It is not necessary to call noConflict when you wrap your code with Webpack because when it is wrapped by Webpack, jQuery detects a CommonJS environment with a DOM and turns on a noGlobal flag internally which prevents leaking into the global space.

webpack.conf.js:

const webpack = require('webpack');
module.exports = {
    entry: {
        main: "./widget.js",
        "main.min": "./widget.js",
    },
    module: {
        rules: [{
            test: /widget\.js$/,
            loader: "imports-loader?define=>false",
        }],
    },
    // Check the options for this and use what suits you best.
    devtool: "source-map",
    output: {
        path: __dirname + "/build",
        filename: "[name].js",
        sourceMapFilename: "[name].map.js",
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            sourceMap: true,
            include: /\.min\.js$/,
        }),
    ],
};

Your widget as widget.js:

var $ = require("jquery");
require("jquery-ui/ui/widgets/datepicker");

var css_link = $("<link>", {
    rel: "stylesheet",
    type: "text/css",
    href: "https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css"
});
css_link.appendTo("head");

$(document).ready(function() {
    console.log("jQuery compare (we want this false)", $ === window.$);
    console.log("jQuery version in widget",  $.fn.jquery);
    console.log("jQuery UI version in widget", $.ui.version);
    $("#end-date").datepicker();
});

index.html:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script>
    <script>
      require.config({
        paths: {
          jquery: "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.min",
          "jquery-ui": "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.0/jquery-ui"
        }
      });
      require(["jquery", "jquery-ui"], function(myJQuery) {
        console.log("jQuery compare (we want this true)", myJQuery === $); 
        console.log("jQuery version main", $.fn.jquery);
        console.log("jQuery ui version main", $.ui.version);
      })
    </script>
  </head>
  <body>
    <input id="end-date">
    <script src="build/main.min.js"></script>

    <!-- The following also works: -->
    <!--
    <script>
      require(["build/main.min.js"]);
    </script>
    -->
  </body>
</html>
like image 161
Louis Avatar answered Nov 15 '22 05:11

Louis