Huh? Angular $injector:nomod error... async / defer script tags fun, $.readyWait/$.holdReady seems broken

357 views Asked by At

=== TL;DR ===

Short version (trust me, I've been poring over this all day):

I've got a big list of scripts, including angular and non-angular stuff (1.2.x). When I try to use LAB.js to load it all, including AlwaysPreserveOrder flag, Angular fires too early. Digging in, it seems Angular is using $(document).ready(/* start angular */). Understandable, script loaders undermine the normal document.ready event.

So, I try to use $.holdReady(true) and $.holdReady(false), but, I get really strange behavior: While $.holdReady(true) does increment $.readyWait as expected, it just won't prevent angular from initializing... and on top of that, if I try to set my own listener for $(document).ready(), it never fires, even though angular is seemingly using that same event to fire...

...To add to the mystery, every once in a blue moon, if I set it to keep doing window.location.reload() on fail until things work, it actually will just work--very rarely. That makes it seem like a race condition, but given that I'm passing in the order flag, I can't imagine what would be racing, nor does that clue seem to fit with everything else, as far as I can tell...

Note that if I switch to non-lab.js version of the file, everything seems to work just fine, including $.readyWait counting, everything (including angular) waiting for $.holdReady(false) being called, the $(document).ready() flag of my own firing, etc.

======LONG VERSION======

I'm trying to produce a minimum viable example, but the more slightly more complicated real world working code is available here, and a broken version is available here--they're still cleaned up to try and make the problem as obvious as possible, without eliminating any potentially confounding factors.

This works:

<body ng-app="mapmycustomersApp" ng-strict-di>
      <script src="build/libraries/LAB.min.js"></script>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.32/angular.min.js"></script>
      <script src="https://code.angularjs.org/1.2.32/angular-resource.min.js"></script>
      <script src="https://code.angularjs.org/1.2.32/angular-route.min.js"></script>

      <script>
         document.write('<script src="build/app.js"></script>')
         document.write('<script src="build/controllers/main.js"></script>')
      </script>
</body>

This doesn't:

<body ng-app="mapmycustomersApp" ng-strict-di>
      <script src="build/libraries/LAB.min.js"></script>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.32/angular.min.js"></script>
      <script src="https://code.angularjs.org/1.2.32/angular-resource.min.js"></script>
      <script src="https://code.angularjs.org/1.2.32/angular-route.min.js"></script>

      <script>
         $LAB.setGlobalDefaults({ AlwaysPreserveOrder: true });
         $LAB
         .script("build/app.js")
         .script("build/controllers/main.js")
      </script>
</body>

And neither does this:

<body ng-app="mapmycustomersApp" ng-strict-di>
      <script src="build/libraries/LAB.min.js"></script>

      <script>
         $LAB.setGlobalDefaults({ AlwaysPreserveOrder: true });
         $LAB
         .script("https://ajax.googleapis.com/ajax/libs/angularjs/1.2.32/angular.min.js")
         .script("https://code.angularjs.org/1.2.32/angular-resource.min.js")
         .script("https://code.angularjs.org/1.2.32/angular-route.min.js")
         .script("build/app.js")
         .script("build/controllers/main.js")
      </script>
</body>

The third one (last one just above) is what I really want.

They produces this error:

Module 'mapmycustomersApp' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

While writing this answer, I made some progress, but ran into a new dead end that I swear feels like a bug in either jquery or angular.

Why? The furthest back I can trace it tells me that angularInit() is being called too soon, well before the app registers itself... That is called at the bottom of the angular file, on line 2232:

  jqLite(document).ready(function() {
    angularInit(document, bootstrap);
  });

jqLite is set to jQuery (I have confirmed this by checking that angular.element === $, which is true, which is evidenced elsewhere in the angular code). So this should just be firing on $(document).ready(). Which has known issues with loaders. Which is why they created $.holdReady().

But for some reason, that doesn't work for me. And I get very weird output.

Here's what I mean: If I move a jquery script tag (2.2.4) to the top of the head, and then call $.holdReady(true) right beneath it, I can see $.readyWait is incremented. (I even can call it a few times for good measure, and watch it go up to four.)

I then stick a .wait(() => $.readyWait(false)) at the end of my .script() calls... but if I console.log right before that, I can see that it's dropped from, say, 4 to 0 already, meaning ready has already fired. The $.readyWait() did nothing.

Valuable clue:

Very rarely, the non-working code works... and then fails again on refresh, with no change. That would seem to indicate a race condition, but I can't understand where that would be, and that wouldn't line up with that else I've seen... yet, it happens. On those times, $.readyWait returns 1 instead of 0.

I know this is a lot, but I feel completely baffled here. What in the world is going on?

Oh, one more very important clue that has baffled me: $(document).ready(_ => console.log("I loaded")) never runs. $(window).load, otherwise identical, runs fine. Doesn't matter where I declare that listener, it just never fires for me, even if I set the listener at the top of the page... and yet it clearly fires somehow, because it's what fires angularInit! My understanding is it should fire if I call on it, even if it has already fired!

...just to show I'm not making some incredibly obvious mistake, all the same code fires as expected (the document ready event, and the holdReady() both do exactly what you'd expect) whenever I switch over to all script tags.

In code:

<!doctype html>
<html>
  <head>
    <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
    <script>
      $(window).load(function() {
         console.log("this fires");
      });
      $(document).ready(function() {
        console.log("this doesn't");
        debugger;
      });
      console.log("holding ready", $.readyWait);
      $.holdReady(true);$.holdReady(true);$.holdReady(true); // needed because we use LAB.js, which messes with Document.ready, which Angular depends on
      console.log('rw, ', $.readyWait)
    </script>

(details: app.js includes

var app = angular.module('mapmycustomersApp' ['ngRoute','ui.bootstrap','ngStorage','ngFileUpload'])
.config(['$routeProvider', '$locationProvider', '$compileProvider', function($routeProvider, $locationProvider, $compileProvider) { // ...

and main.js includes

app.controller('MainCtrl', ['$scope', '$http', // ...

All code is functional, I'm just trying to move everything over to LAB.js, and find myself unable to. Other comments: switching to angular 1.6.4 just for fun does not have an effect (except to fail loading completely on those rare successes because $http().success is not a function, so they must have changed that API in later version), and we're on the latest version of jquery within version 2, and switching to 3+ throws other incompatibility errors.

1

There are 1 answers

0
Kyle Baker On

I was correct in identifying the problem. In a lengthy discussion with Kyle Simpson, he pointed out that the use of $.ready in the angular 1.2x source code is a common mistake based on incorrect assumptions that makes loading javascript sources asynchronously non-viable.

Using $.holdReady() did eventually work, for some reason. I'm not sure why it didn't, but I suspect I was accidentally including a jquery dependency twice, overwriting the previous readyCount.