5

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.

4
  • Is it possible that there are multiple versions of jQuery being loaded onto the page? I ask because I see the window.jQuery.fn.jquery !== '3.2.1' condition in your jQuery-loading if() — and when there are more than one version of jQuery on a page, .noConflict() returns the globally available one (which might not be the one that jQuery UI extended). Commented Oct 24, 2017 at 17:56
  • Yes there is 2 versions.. But we load ui on the version we have loaded. Commented Oct 24, 2017 at 18:09
  • Do you have access to the config file for your project's RequireJS build? The solution might lie in defining (your version) of jQuery and jQuery UI as path properties there, then conditionally require() them as needed (making sure to set the $ within your module's scope to the RequireJS-managed version of jQuery — that has the added benefit of obviating the need for the .noConflict(). Commented Oct 27, 2017 at 10:15
  • @ItoPizarro RequireJS is not part of Lee's project. it is part of the "partner" code that exists independently from the widget code shown in the question. Besides, what you describe does not eliminate the race condition issue that I describe in my answer. As soon as you have two versions of jQuery you want to load asynchronously, and that leak into the global space (which they do if they are loaded as AMD modules), then you necessarily have a race condition when other code gets jQuery from the global scope. No amount of fiddling with the RequireJS configuration that eliminates it. Commented Oct 28, 2017 at 12:59

2 Answers 2

3

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>
Sign up to request clarification or add additional context in comments.

Comments

-1

I think problem is with jQueryWidget = jQuery.noConflict(true);

true means remove all jQuery variables from the global scope.

jQuery.noConflict( [removeAll ] ) removeAll Type: Boolean A Boolean indicating whether to remove all jQuery variables from the global scope (including jQuery itself).

[noconflict][1]

One thing we can try removing the true boolean parameter, Let know if it helps.

UPDATE 2: Below approach should not require any partner code changes

<!DOCTYPE html>
        <html>

        <head>
            <title></title>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script>
            <script type="text/javascript">
            (function() {

                // Localize jQuery variable
                var jQueryWidget;
                /*
                *
                *
                *
                    This is plugin's require config. Only used by plugin and
                    will not affect partner config.
                *
                *
                */
                var requireForPlugin = require.config({
                    context: "pluginversion",
                    paths: {
                        "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min",

                        "jquery.ui.widget": "https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min"

                    }
                });
                requireForPlugin(["require", "jquery", "jquery.ui.widget"], function() {
                    /******** Load jQuery if not present *********/
                    if (window.jQuery === undefined || window.jQuery.fn.jquery !== '3.2.1') {
                        scriptLoadHandler();
                    } else {
                        loadJQueryUi();
                    }

                    function scriptLoadHandler() {
                        loadJQueryUi();
                    }

                    function loadJQueryUi() {
                        jQueryWidget = jQuery.noConflict(true);
                        setHandlers(jQueryWidget);
                        var css_link = jQueryWidget("<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($) {
                        $('#end-date').on('click', function() {
                            alert('JQUERY--' + $().jquery);
                            alert('JQUERY UI--' + $.ui.version);
                            $('#end-date').datepicker({
                                dateFormat: "M dd, yy",
                                minDate: '+1D',
                                numberOfMonths: 1,
                            });
                        });
                    }
                });
            })();
            </script>
            <script>
            //SAMPLE PARTNER APPLICATION CODE:

            /*
            *
            *
            *
                This is partner's require config
                which uses diffrent version of jquery ui and
                jquery
            *
            *
            */
            require.config({
                paths: {
                    "jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.min",

                    "jquery.ui.widget": "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.0/jquery-ui"

                }
            });
            require(["jquery", "jquery.ui.widget"], function() {
                $('#btn').on('click', function() {
                    alert('JQUERY--' + $().jquery);
                    alert('JQUERY UI--' + $.ui.version);
                });
            })
            </script>
        </head>

        <body>
            <div><span>FOCUS</span>
                <input type="text" name="" id="end-date" />
            </div>
            <button id="btn" style="width: 100px; height:50px; margin:10px">click me</button>
        </body>

        </html>

I have modified the plugin code, so that it uses its own jquery and jquery ui version(I am making use of the requirejs here).

Also for demo I have added a sample partner script which gives alert on button click. you can see without modifying the partner code and requirejs config, plugin can work now.

Both plugin and partner code have there independent jquery and jquery ui versions.

Hope this helps.

References: Using Multiple Versions of jQuery with Require.js and http://requirejs.org/docs/api.html#multiversion

UPDATE3: using webpack and imports loader We can use webpack to bundle the plugin js code in that case plugin will have its own version of jquery, we have to change the way the plugin is being build.

Install webpack, jquery, imports loader and jquery-ui using npm and build it, below is the sample code:

main.js used imports loader to make define as false

require('imports-loader?define=>false!./app.js');

app.js which includes the plugin code and it adds the required dependencies

 (function() {

     var $ = require('jquery');
     require('jquery-ui');
     require('jquery-ui/ui/widgets/datepicker.js');

     function scriptLoadHandler() {
         loadJQueryUi();
     }

     $(document).ready(function() {
         scriptLoadHandler();
     });

     function loadJQueryUi() {
         setHandlers();
         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');
     }

     function setHandlers() {
         $('#end-date').on('click', function() {
             alert('JQUERY--' + $().jquery);
             alert('JQUERY UI--' + $.ui.version);
             $('#end-date').datepicker({
                 dateFormat: "M dd, yy",
                 minDate: '+1D',
                 numberOfMonths: 1,
             });
         });
     }
 })();

webpack.config.js

  var webpack = require('webpack');

  module.exports = {
  entry: "./main.js",
  output: {
  filename: "main.min.js"
     }
  };

sample.html

<!DOCTYPE html>
<html>

<head>
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script> 
    <script src="main.min.js"></script> 
    <script>
    //SAMPLE PARTNER APPLICATION CODE:

    /*
    *
    *
    *
        This is partner's require config
        which uses diffrent version of jquery ui and
        jquery
    *
    *
    */
    require.config({
        paths: {
            "jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.min",

            "jquery.ui.widget": "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.0/jquery-ui"

        }
    });
    require(["jquery", "jquery.ui.widget"], function() {
        $('#btn').on('click', function() {
            alert('JQUERY--' + $().jquery);
            alert('JQUERY UI--' + $.ui.version);
        });
    })
    </script>
</head>

<body>
    <div><span>FOCUS</span>
        <input type="text" name="" id="end-date" />
    </div>
    <button id="btn" style="width: 100px;"></button>
</body>

</html>

after doing a webpack it will generate a main.min.js file which is included in the sample.html file

11 Comments

Sorry @Lee I have overlooked the question, I have updated my answer. Hope this helps.
@gusaindpk Your edit is just as bad as your original answer. shim won't solve anything because jQuery UI calls define, and shim is for code that does not call define. Search jQuery UI's code for a call to define, you'll see define(["jquery"], ... on the first line of code right after the initial comments.
Also don't forget. We are only passing them the widget code and not directly changing there code.
@lee correct shim is not required for jquery ui, also just one doubt, is there specific reason that plugin itself loads jquery and jquery-ui, a better option would be let the partner load the required dependencies ex jquery and jquery ui. it can be simple script tags or via require js. that way your plugin will not require any change in future also. thoughts?
It was me who corrected you regarding shim. Seems to me the OP wants to achieve integration without requiring that the developers incorporating the OP's widget modify their RequireJS configuration just to accommodate the widget. Moreover, the OP's code takes into account the possibility that the page, prior to using the widget, uses a version of jQuery that is different from the one the widget wants to use. That's important. If the widget was developed for jQuery 3.x, it cannot readily use an earlier version. Conversely, if the page used an earlier version, it may not work with 3.x.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.