1

I am trying to create widget using jquery. So I create a js file, that is responsible for spitting html to any website (say xyz.com) . That js file is as follows:

// @credit to: 
// http://alexmarandon.com/articles/web_widget_jquery/
// http://stackoverflow.com/a/2190927/1560470

var MYCARD = MYCARD || (
  function() {
    // Localize variables
    var jQuery, _cid;

    return {
      init : function (id) {
        _cid = id; 
        /******** Load jQuery if not present *********/
        if (window.jQuery === undefined || window.jQuery.fn.jquery !== '1.11.2') {
          var script_tag = document.createElement('script');
          script_tag.setAttribute("type","text/javascript");
          script_tag.setAttribute("src", "http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js");
          if (script_tag.readyState) {
            script_tag.onreadystatechange = function () { // For old versions of IE
              if (this.readyState == 'complete' || this.readyState == 'loaded') {
                this.scriptLoadHandler();
              }
            };
          } else {
            script_tag.onload = this.scriptLoadHandler;
          }
          // Try to find the head, otherwise default to the documentElement
          (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
        } else {
          // The jQuery version on the window is the one we want to use
          jQuery = window.jQuery;
        }
      },
      scriptLoadHandler: function () {
        // Restore $ and window.jQuery to their previous values and store the
        // new jQuery in our local jQuery variable
        jQuery = window.jQuery.noConflict(true);
      },
      render: function() {
        jQuery(document).ready(function($) { 
          /******* Load CSS *******/
          var css_link = $("<link>", { 
            rel: "stylesheet", 
            type: "text/css", 
            href: "mystyle.css" 
          });
          css_link.appendTo('head');          

          /******* Load HTML *******/
          var jsonp_url = "https://twitter.com/"+_cid+"?modal=true&callback=?";
          $.getJSON(jsonp_url, function(data) {
            $("#"+_cid+"-widget-container-for-card").html("This data comes from twitter: " + data.html);
          });
        });
      }
    };
  }()
);

And in xyz.com's page, user can insert my widget as follows: (My goal is to pass an id from each widget, so that widget can render accordingly. Below you can see MYCARD.init("cnn"); where cnn is the id)

<html>
<head></head>
<body>
  <h2>CNN on Twitter</h2>
  <script src="http://widgetwebsite.com/embed.js" type="text/javascript"></script>
  <script type="text/javascript">
    MYCARD.init("cnn");
    MYCARD.render();
  </script>
  <div id="cnn-widget-container-for-card"></div>
  <h2>BBC on Twitter</h2>

  <script src="http://widgetwebsite.com/embed.js" type="text/javascript"></script>
  <script type="text/javascript">
    MYCARD.init("bbc");
    MYCARD.render();
  </script>
  <div id="bbc-widget-container-for-card"></div>

  <h2>CNN on Twitter</h2>
  <script src="http://widgetwebsite.com/embed.js" type="text/javascript"></script>
  <script type="text/javascript">
    MYCARD.init("cnn");
    MYCARD.render();
  </script>
  <div id="cnn-widget-container-for-card"></div>
</body>

However when I run this, I get Uncaught TypeError: jQuery is not a function , occurred at jQuery(document).ready(function($) { while jQuery should have been initialized at init method. Any idea what is going wrong here?

Please note, user may include same widget multiple times on his page (by mistake), and if that happens, then this code should handle that corner case.

JVK
  • 3,782
  • 8
  • 43
  • 67

2 Answers2

1

The problem here is that you assume that the inline script is executed after your script has finished loading jQuery, but that is not the case. Here is the sequence of actions that is actually happening:

  1. embed.js script is executed and initiate jQuery loading in the background
  2. your inline script is executed and fails because it requires jQuery
  3. jQuery loading completes, but is now useless since your inline script has already failed

Frist suggestion

In your case, if this is all you have to pass to your script, you may consider using data- attributes so you'd have only your script inclusion tag looking like this:

<script src="http://widgetwebsite.com/embed.js"
        type="text/javascript"
        data-my-card-id="cnn"></script>

Then in your code, when jQuery has loaded, your look for $("script[data-my-card-id]") tags and fetch the values.

Second suggestion

If for some reason you really want to use an inline script, it must wrap its content within a callback that will get called from your code:

 <script type="text/javascript">
     var MYCARD_ready_callback = function() {
       MYCARD.init("cnn");
       MYCARD.render();
     };
 </script>

Then, once jQuery has loaded, you call your callback:

  scriptLoadHandler: function () {
    // Restore $ and window.jQuery to their previous values and store the
    // new jQuery in our local jQuery variable
    jQuery = window.jQuery.noConflict(true);
    // Call callback define in inline script
    MYCARD_ready_callback();
  }

Multiple inclusion support

To support multiple inclusions the approach I take is to add some guard in my widget main function that checks for a global variable:

var MYCARD = MYCARD || (
  function() {
     if (window.MYCARD_is_already_loaded) {
         return;
     }
     window.MYCARD_is_already_loaded = true;
     // rest of the code
     // ...
  }()
);

Then in your widget code you deal with multiple inclusions, either by fetching the values from data- attributes or by executing callback handlers, in which case you need an array of callbacks rather than a single callback:

<script type="text/javascript">
     var MYCARD_ready_callbacks = MYCARD_ready_callback || [];
     var MYCARD_ready_callbacks.push(function() {
       MYCARD.init("cnn");
       MYCARD.render();
     });
 </script>

Final advice

Always think of the implications of asynchronous loading :)

Alex Marandon
  • 4,034
  • 2
  • 17
  • 21
  • Thanks Alex for detailed reply with snippet. Appreciate it. I will test it and let you know the result. – JVK Sep 23 '15 at 01:46
  • I would prefer your second suggestion, because I don't want third party website having any script with `data-mycard-id` (who knows if that ever happens :) ), So I went through your second suggestion (see here http://bit.ly/1KCVKLy ) with multiple inclusion and unfortunately it did not work. The reason is (as I believe), you have array of callbacks, but who will fire them? Callbacks array is just storing multiple anonymous functions but never gets called from anywhere. I put debugger at the first line inside `init` function, and that never got invoked – JVK Sep 24 '15 at 06:35
  • 1) The chance of name collision with `data-` attributes is no different than with your global JS variable. In both cases you need to pick a name that's specific enough to make it very unlikely to clash with the host page, for example `data-companyname-projectname-attrname`. If you want to be really sure, stick a UUID in it ;-) 2) You need to call the anonymous functions stored in the array by iterating on the array and calling them one by one. See my comment on your gist. – Alex Marandon Sep 24 '15 at 08:06
0

You can't call your render method until jQuery.js is loaded

Instead of calling it right after init() in global space, call it internally after you have the load complete and scriptLoadHandler() has been called since that's where you are defining your local jQuery variable

charlietfl
  • 170,828
  • 13
  • 121
  • 150
  • I made changes per your suggestion, see here http://bit.ly/1Mnkk67 but that seems failing the check for jquery inclusion now – JVK Sep 22 '15 at 03:03
  • hard to help without being able to run in browser console. Set breakpoints and step through it – charlietfl Sep 22 '15 at 03:11