laurelnaiad / angular-couch-potato

Lazy-Load and Register Components in AngularJS Applications
Other
104 stars 19 forks source link

Enter state after vendor library is loaded. #12

Open mparisi76 opened 10 years ago

mparisi76 commented 10 years ago

I have a situation where my app needs to enter a certain state only after a vendor library has run it's own proprietary script loading mechanism. Basically, I need to listen for the following:

document.on('mp_ready', function() { 
    //resolve
});

What is the proper way to accomplish this?

Thanks!

laurelnaiad commented 10 years ago

This isn't really related to couch potato.

If you are responding to non-Angular events and you want Angular to know about it (usually you do), then wrap your event handler code in $rootScope.$apply(function () { /* your code here */ });

Couch potato, like the rest of the angular components in your app, will then be "in the loop" as to what happens in your event handler.

I'll close this since it isn't really a couch potato issue. If you feel like there is a couch potato aspect to this that I'm missing, please feel free to respond.

EDIT: I should have been a bit more thorough -- if the event you're talking about takes place within the context of a specific view, you can use the $scope associated with that view rather than $rootScope.... basically the gist of it is that whatever scope should be applicable to the event is the one that should know about it. $rootScope if its a global-scoped event, and the $scope of the view if the component is some UI widget in a particular view...

mparisi76 commented 10 years ago

I'm not sure I understand. BTW, this isn't really an issue, just a question. In your examples you have a bunch of states, which have resolve: {dummy:blahblah}. I don't want the state to resolve until after the vendor library has run it's script loading (at which time the 'mp_ready' event is fired). Wouldn't this go inside the resolve:{}?

laurelnaiad commented 10 years ago

Ah, ok. Still not technically a couch potato issue, but at least now I understand your question a bit better.

It seems you'd want the function that you give to the resolve property to return a promise that is resolved by the event handler for the event you specified above. This means that you set up the promise, you return it, and then you hang on to the deferred object so that the event handler can call deferred.resolve().

Because the promise will be hanging unresolved until the event occurs, the resolution process will pause while it waits for that event. Once the event fires, the handler should call resolve on the deferred object, thus resolving the promise, and things move forward.

mparisi76 commented 10 years ago

That's exactly right. Before I switched from routes to states, I had the following, in my route config:

resolve: {
                    load: ['$q', '$rootScope', function ($q, $rootScope) {
                        var deferred = $q.defer();
                        require([
                                'someFile',
                                'anotherFile',
                                ], function () {
                            $rootScope.$apply(function () {
                                deferred.resolve();
                            });
                        });
                        return deferred.promise;
                    }]
                }

Is there an example of how I could accomplish this same thing using ui-router and couch potato?

mparisi76 commented 10 years ago

BTW, I realize that this isn't a couch potato question, but more of a ui-router/angular question. I apologize for that.

laurelnaiad commented 10 years ago

I think you may want to use two resolve properties -- one that deals with loading the components you need via couch potato, and another that handles the promise related to the event you're trying to wait for...

Alternatively, if one of the couch potato components you're waiting for depends on the external event happening before it can be considered initialized, then you'd probably shift the burden of listening for the event into the defining module itself (i.e. the require.js module that defines the couch potato component which depends on the event from the external lib).

EDIT: there might be an issue with that alternative, in that require.js doesn't want to wait for promises and wouldn't hold up for an external event either.... so probably the former idea is better than the latter.

laurelnaiad commented 10 years ago

If you want to combine waiting for your components with waiting for the external event into one resolve function, it might look something like:

resolve: {
  load: ['$q', '$rootScope', function ($q, $rootScope) {
    var overallDeferred = $q.defer();
    var eventDeferred = $q.defer();
    var requireDeferred = $q.defer();

    $q.all([eventDeferred.promise, requireDeferred.promise])
        .then(overallDeferred.resolve);

    require([
    'someFile',
    'anotherFile',
    ], function () {
      $rootScope.$apply(requireDeferred.resolve);
    });

    document.on('mp_ready', function () {
      $rootScope.$apply(eventDeferred.resolve);
    });

    return overallDeferred.promise;
  }]
}

Note: edited since first posted to reflect that it's the promises that are passed to $q.all, not the deferred objects.

mparisi76 commented 10 years ago

Great, thanks for that. That might work in ngRoute but I'm using ui.router/scs.couch-potato and resolveDependencies(). Would it work the same way?

laurelnaiad commented 10 years ago

That might look something like:

resolve: {
  load: ['$q', '$rootScope', '$couchPotato', function (
    $q, $rootScope, $couchPotato
  ) {
    var overallDeferred = $q.defer();
    var eventDeferred = $q.defer();
    var requireDeferred = $q.defer();

    $q.all([eventDeferred.promise, requireDeferred.promise])
        .then(overallDeferred.resolve);

    $couchPotato.resolveDependencies(['someFile', 'anotherFile'])
        .then(requireDeferred.resolve);

    document.on('mp_ready', function () {
      $rootScope.$apply(eventDeferred.resolve);
    });

    return overallDeferred.promise;
  }]
}
mparisi76 commented 10 years ago

It's weird, I'm tweaking what you have above, but resolveDependencies() never gets called on couch potato.. I'm trying to figure out why.

mparisi76 commented 10 years ago

Ok, resolveDependencies() does get called, however, the requireDeferred never gets resolved, and I can't log anything after that..

laurelnaiad commented 10 years ago

You can put a breakpoint in your browser around here: https://github.com/stu-salsbury/angular-couch-potato/blob/master/src/couchPotato.js#L166 to figure out what's happening. If it's being called but it's not resolving its promise then something may be erroring out. In looking at the code, I can see that it probably should have a try/catch in it that rejects the promise on catch. Since it's not there, you may find that an error is happening, causing the promise to just die on the vine.

laurelnaiad commented 10 years ago

If an error is your problem, you could at least get better notification if you change the couch potato function to look like this (untested):

function resolveDependencies(dependencies, returnIndex, returnSubId) {
  function delay($q, $rootScope) {

    var defer = $q.defer();

    try {
      require(dependencies, function() {
        var args = Array.prototype.slice(arguments);

        var out;

        if (returnIndex === undefined) {
          out = arguments[arguments.length - 1];
        }
        else {
          argForOut = arguments[returnIndex];
          if (returnSubId === undefined) {
            out = argForOut;
          }
          else {
            out = argForOut[returnSubId];
          }
        }

        defer.resolve(out);
        $rootScope.$apply();
      });
    }
    catch (err) {
      defer.reject(err);
    }

    return defer.promise;
  }

  delay.$inject = ['$q', '$rootScope'];
  return delay;

}

.... I'd deal with this myself, but I'm kinda busy ATM...

laurelnaiad commented 10 years ago

I think we're back in couch potato territory, so I'm reopening.... :)

mparisi76 commented 10 years ago

It's funny, I tried that before your post. I put one at 165 and 162 and neither of them are being hit. I will keep digging.

laurelnaiad commented 10 years ago

If they aren't being hit, but resolveDependencies is being hit, then there must be an issue with calling it from the service $couchPotato as opposed to the provider $couchPotatoProvider.

If you're working from the sample code, you probably already injected $couchPotatoProvider at the same time you injected $stateProvider, so it should be available through closure.... this isn't necessarily a kosher way to go, but given what seems like a couch potato bug I'd recommend it for now (adapt as needed):

resolve: {
  load: ['$q', '$rootScope', function (
    $q, $rootScope
  ) {
    var overallDeferred = $q.defer();
    var eventDeferred = $q.defer();
    var requireDeferred = $q.defer();

    $q.all([eventDeferred.promise, requireDeferred.promise])
        .then(overallDeferred.resolve);

    // $couchPotatoProvider is still available due to JS closure from above
    $couchPotatoProvider.resolveDependencies(['someFile', 'anotherFile'])
        .then(requireDeferred.resolve);

    document.on('mp_ready', function () {
      $rootScope.$apply(eventDeferred.resolve);
    });

    return overallDeferred.promise;
  }]
}

Does that help?

mparisi76 commented 10 years ago

A note, if I remove '.then(requireDeferred.resolve);' after resolveDependencies(), any code after that line executes. If I leave it in there, nothing executes after it. Might be interesting.

Also, breakpoint at 190 DOES get hit, but breakpoints at 162 and 165 do not get hit.

I will experiment with your previous comment. Thanks!

laurelnaiad commented 10 years ago

btw, when I say "not kosher" I mean that I lose style points for making this the only way to use the resolveDependencies function, not that your code suffers. The same code is doing the registering one way or the other, I just shouldn't be forcing you to use closure to maintain a reference to the provider.

laurelnaiad commented 10 years ago

Hmmmm....

if I remove '.then(requireDeferred.resolve);' after resolveDependencies(), any code after that line executes. If I leave it in there, nothing executes after it.

makes me wonder if you're resolve function is the one throwing an unhandled error. You might also want a try/catch of your own...

laurelnaiad commented 10 years ago

Haha.. It's been a while since I looked at this code. I notice that there is an inner function that is returned by resolveDependencies (named internally as "delay").... which is all well and good when you're setting up your resolve property at config time (as is the case in the sample) since what's left after calling it is a function that itself does the run-time work.

Not so good if all of your code is running at Angular run time. Forget the stuff about switching between the provider and the service.... try this (call the function returned by resolveDependencies):

resolve: {
  load: ['$q', '$rootScope', '$couchPotato', function (
    $q, $rootScope, $couchPotato
  ) {
    var overallDeferred = $q.defer();
    var eventDeferred = $q.defer();
    var requireDeferred = $q.defer();

    $q.all([eventDeferred.promise, requireDeferred.promise])
        .then(overallDeferred.resolve);

    var resolveFunc = $couchPotato.resolveDependencies(['someFile', 'anotherFile']);
    resolveFunc().then(requireDeferred.resolve);

    document.on('mp_ready', function () {
      $rootScope.$apply(eventDeferred.resolve);
    });

    return overallDeferred.promise;
  }]
}
laurelnaiad commented 10 years ago

(the bug is that if called at run-time, the inner function should be executed right away without you having to do it -- the workaround above should get you past that for now)

mparisi76 commented 10 years ago

Ahh, I see what you mean. The problem with that, is now $q and $rootScope are undefined in delay()

laurelnaiad commented 10 years ago

(Coincidentally or not) I think you happen to have them already! :)

How about:

resolve: {
  load: ['$q', '$rootScope', '$couchPotato', function (
    $q, $rootScope, $couchPotato
  ) {
    var overallDeferred = $q.defer();
    var eventDeferred = $q.defer();
    var requireDeferred = $q.defer();

    $q.all([eventDeferred.promise, requireDeferred.promise])
        .then(overallDeferred.resolve);

   var resolveFunc = $couchPotato.resolveDependencies(['someFile', 'anotherFile']);
   resolveFunc($q, $rootScope)
        .then(requireDeferred.resolve);

    document.on('mp_ready', function () {
      $rootScope.$apply(eventDeferred.resolve);
    });

    return overallDeferred.promise;
  }]
}
mparisi76 commented 10 years ago

YES!! You rock! Finally got this to work, I tweaked your solution just a hair, using $q.all(), do you see any issues in doing so?

resolve: {
            load: ['$q', '$rootScope', '$document', '$couchPotato', function ($q, $rootScope, $document, $couchPotato) {
              var eventDeferred = $q.defer();
              var requireDeferred = $q.defer();

              var promises = [requireDeferred.promise, eventDeferred.promise];

              var resolveFunc = $couchPotato.resolveDependencies([
                'someFile',
                'anotherFile'
                ]);
                resolveFunc($q, $rootScope).then(requireDeferred.resolve);

              $document.on('mp_ready', function () {
                $rootScope.$apply(eventDeferred.resolve);
              });

              return $q.all(promises);
            }]
          },
laurelnaiad commented 10 years ago

Nope: 6 of one, half-dozen of the other. $q.all returns the promise you care about so that should work. Yours is cleaner. :thumbsup: