Ordered application initialization with promises in a dedicated service

939 views
Skip to first unread message

Tobias Gesellchen

unread,
Oct 28, 2013, 6:55:44 AM10/28/13
to ang...@googlegroups.com
Hi group,

as already asked or suggested in several posts, you sometimes need to wait for http backend calls to finish before your AngularJS app is regarded as being initialized and usable.

We also stumbled over the problem of $watch triggers or $on events being fired too early (before the http requests had been finished loading data) in the application lifecycle, so we have built a dedicated "initialization service" which enables us to configure the loading order of services and helps us configuring our controllers and $scopes to be initialized correctly.

You can find a more thorough description at our blog an a working demo at jsfiddle.

We would like to get some feedback on our implementation. Please have a look at our code and provide some feedback in this group. Questions are welcome as well!

Thanks!

Daniel Tabuenca

unread,
Oct 28, 2013, 1:11:31 PM10/28/13
to ang...@googlegroups.com
Personally, I would have simply created my own watch function that wraps $scope.watch() and only calls through to the callback if oldValue!=newValue. The watches can then be attached on promise resolution and the problem with $watch getting called the first time is also eliminated.  I'm a bit skeptical of the value of an initialization phase beyond just using promises and resolves. Do you find much additional value in this pattern beyond the avoiding of the first watch call?  

Tobias Gesellchen

unread,
Oct 28, 2013, 2:01:16 PM10/28/13
to ang...@googlegroups.com
We also started with simple checks like oldValue != newValue and newValue != undefined, but at some point we didn't want all those similar checks to be implemented in every $watch handler.

Independent to the $watch handlers, we also had code spread across our app, which had to be run in a particular order (at least during initialization), due to functional constraints. Our app is cut into several modules, which we try to be as encapsulated as possible, so that we needed some way to synchronize shared state by using shared models. Some change events (say: $watch triggers or $on events) could trigger expensive backend calls, so we focused on issuing them only when the user triggers them. The init service helps us to collect resolved values and to pass them back to our controllers, without triggering new change events. Just as you said, we use promises and resolves, but we extracted the logic of ordering runtime dependencies on resolved data into a central place without moving controller specific code into a common service.

As mentioned in the article, we also suggest thinking about a dedicated init phase, because it minimizes confusion in our code and especially in our tests. We need some more tests or different test setup, but by explicitly testing "not initialized" and "initialized" application state, we could reduce some code parts checking for issues like oldValue != newValue in $watch handlers. By conceptually splitting both phases, we could focus on relevant checks according to each phase. The readability of our code has improved, too.

I wouldn't say that we only avoided the first $watch call, but we also changed our mind implementing application logic. Nevertheless, I wouldn't recommend a dedicated initialization phase, when the app was too small. Splitting phases can make code more complex, but I guess our app was big enough for such a concept. I didn't have any chance to compare our app to other ones, regarding size or complexity. Just as a rough hint: our app has about 1.7K statements, according to our coverage tool (karma-coverage/istanbul), spread across 25 modules.

There are similar discussions about the topic with slightly different motivation, e.g. see the pull request about factories returning promises. I guess there's some need to improve AngularJS' handling of "asynchronous initialization", but most requirements only overlap, so it's hard to find a common solution for everyone.

What is your experience with a more complex module structure or bigger applications? Did you have similar problems and addressed them just by checks in your $watch handlers or did you add some more logic?
Message has been deleted

Daniel Tabuenca

unread,
Oct 28, 2013, 4:13:57 PM10/28/13
to ang...@googlegroups.com
Currently working on fairly large single page application but just haven't run into issues with initialization. We simply include any hard requirements in the resolve block for the route, which guarantees that those promises will be resolved before our controller is initialized (although we prefer limiting the amount of promises in the resolve, allowing the page to render and things to fill in as they come in whenever possible). 

We have only ran into a few places where we've had to do the oldValue != newValue type checks on a watch, for the most part, our watches are idempotent. What I was suggesting, is that if you do find yourself having to do this in many places, you could just generalize it into a wrapper function (similar to how you are now doing watchAfterInit() but rather than checking an "initialized" flag, it would simply check old and new value. Something like:

function watchChanges(scope, expression, listener, deepEqual) { scope.$watch(expression, function (newValue, oldValue, listenerScope) { if (oldValue !== newValue && newValue !== undefined ) { listener(newValue, oldValue, listenerScope); } }, deepEqual); }

Maybe it's just that your example is relatively simple compared to more complicated needs in your application, but it's hard for me to see why this:

init('simpleController', [myService.getOptions()], function(result) {
$scope.options = result.data.options;
  $scope.selectedOption = 0;
});

init.watchAfterInit($scope, 'selectedOption', function(newValue, oldValue) {
// handle selection change ...
   console.log("selection: " + $scope.selectedOption);
});

Is better than the simpler:

myService.getOptions().then(function(result) {
$scope.options = result.data.options;
  $scope.selectedOption = 0;
watchChanges($scope, 'selectedOption', function(newValue, oldValue){
// handle selection change ...
   console.log("selection: " + $scope.selectedOption);
});



Tobias Gesellchen

unread,
Oct 30, 2013, 5:49:08 AM10/30/13
to ang...@googlegroups.com
You're right in the possibility of using a generalization like watchChanges(). But as I tried to explain, the init service does more than only checking for actual changes. I'll try to explain other cases where a dedicated init phase helps us minimizing unexpected events/triggers and keeping a consistent state:
- Some controllers or services maintain their internal scope or shared models with the help of "update()" functions - mostly being called in a $watch trigger. In order to keep our code DRY, we reuse those update functions during the initializing phase. Some update functions change $scope values in a manner that a newValue is always different to an oldValue, so checking for equality like in watchChanges() is not enough.
- Another problem was modifying a $route search param during the initializing phase. Some code parts set a clean $route search as part of their init, but other code parts listen to $routeChange events. We didn't want tighter coupling of those modules, so we needed another way of disabling the $routeChange event listeners. That also explains, why we preferred not to write our init chain in the route resolve block, because we wanted Angular to reach a consistent state before we were going to modify it, dependent on the current user authorities.
- When disabling event listeners and $watch handlers, we needed a way of notifying controllers that their dependencies or required data was available. That's why we implemented an ordered chain of init calls like this (don't be surprised about the naming, it's only an example ;-) ):
      module1.init()
        .then(module2_createFilter.init)
        .then(module3.init)
        .then(module2_applyFilter.init)
        .then(sort.init)
        .then(listView.init)
        .then(broadcastAppInitialized);
That "init chain" is probably similar to the resolve block in your app, we only implemented it in a dedicated service.

Do the examples make our motivation for an init phase clearer now? You might perhaps still have better suggestions?

doug g

unread,
Feb 12, 2014, 9:31:27 AM2/12/14
to ang...@googlegroups.com
Hi

First of all, thanks for your code example. I was running into the problem of watchers being fired over and over on an app Im developing, and your ordered initialization code helped a lot.

Sorry for what is probably a newbie question, but I am looking at the broadcastAppInitialized function, specifically using $browswer.defer for setting initialized=true and broadcasting final initialization. For one, I cant seem to find the documentation for $browser anywhere, and I even see a post that suggests it might be deprecated (see https://github.com/angular/angular.js/issues/532). Is there some hidden documentation somewhere?

Also, I tried changing the function to:

 var broadcastAppInitialized = function(){
           initialized = true;
           $rootScope.$broadcast( APPLICATION_INITIALIZED );
 }

(This makes unit testing a little easier as I dont have to mock $browser.defer to set initialized=true)

I can see no difference in how the code behaves using this simpler method. Can I ask, what problem were you trying to solve by wrapping this in the $browser.defer?

Thanks 

Doug

Tobias Gesellchen

unread,
Feb 12, 2014, 2:33:10 PM2/12/14
to ang...@googlegroups.com
Hi Doug,

thanks for your feedback!

It seems like the documentation for $browser is not linked on the table of contents and moreover doesn't contain any content. You can find a link at the $timeout documentation under the section "Dependencies". $timeout uses $browser internally and you're right that we might have used an alternative service (like $timeout). I didn't check in detail, but I guess the documentation has changed since we wrote the code. Our blog article also contained a deep link for the browser event loop to a currently missing section, which has been updated now. Regarding your question about the deprecation: only $defer has been deprecated in favor of $timeout, but $browser.defer is still available and not deprecated. Quite confusing :-)

Finally about the background of why we wrapped the event broadcast in $browser.defer:
I suggest you to read the details about the browser event loop first - it helps a bit understanding the Angular way of handling model changes. The relevant bit says that "the $digest loop keeps iterating until the model stabilizes, ...". You might now expect that the APPLICATION_INITIALIZED event would be broadcasted to all listeners inside the same $digest loop as the initialization promise chain.
The listeners could probably update some model or trigger other actions, which depend on a completely initialized and stable model. But performing those actions inside the "app initializing loop" would risk that some details could be in a non initialized state. That's why we forced Angular to finish the "app initializing loop" and start a new $digest loop for the APPLICATION_INITIALIZED event.

In most cases there won't be any problem with removing the $browser.defer call. As long as it works for you, the unit testing aspect is probably more relevant. We only had some edge case with route updates and backend calls which were not allowed to happen before the complete application had been initialized. So it depends on how much your initialized event listeners do.

I hope your questions have been answered. Did I miss some detail or do you have any additional questions?

Regards
Tobias

doug g

unread,
Feb 12, 2014, 2:42:38 PM2/12/14
to ang...@googlegroups.com
Hi Tobias

Thanks so much for your thoughtful reply. Now I understand the reason for that $browser.defer, and it makes sense that you implemented it that way. And you are right - Im not relying on the broadcast event, but rather the deferred watchers, so the unit tests are more important to me. But Ill keep an eye out for that kind of funkiness :-)

Take care and be well.

Yours,
Doug


--
You received this message because you are subscribed to a topic in the Google Groups "AngularJS" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/angular/6CxwIL4QSfY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular.
For more options, visit https://groups.google.com/groups/opt_out.

Tobias Gesellchen

unread,
Feb 18, 2014, 2:33:08 PM2/18/14
to ang...@googlegroups.com
There's an issue at GitHub trying to find solutions for asynchronous initialization: https://github.com/angular/angular.js/issues/5854
You might try to explain details of your requirement there.

doug g

unread,
Feb 18, 2014, 4:16:48 PM2/18/14
to ang...@googlegroups.com
Hi Tobias

Happy to support the community that way, but can you give me a few more details about what you might be looking for from that? My requirements were actually not much more than an ordered and controlled initialization process, and a way to avoid triggering multiple $watches. Your solution does both quite well

Doug

Tobias Gesellchen

unread,
Feb 18, 2014, 4:27:56 PM2/18/14
to ang...@googlegroups.com
I guess the Angular developer team only needs some feedback on which use cases are needed and which ones would be great to be supported out of the box. So just describing your use case might suffice. I personally wouldn't expect you to write down a detailed user story, but more a kind of "what didn't work and which feature was missing".
But you shouldn't feel forced by me to explain yourself more than necessary, I only wanted you to know about the issue item and give you the option to take a chance improving Angular :)

Thank you so far!
Tobias
Reply all
Reply to author
Forward
0 new messages