Tuesday, October 31, 2017

RequireJS effecting an embedded jquery-ui widget

Leave a Comment

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.

2 Answers

Answers 1

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> 

Answers 2

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

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment