1

BACKGROUND

As my app grows I’m struggling more and more with AngularJS promise synchronization / sequencing across multiple controllers and services. In my example below I have an articles controller ArticleController and related service ArticleDataService that in the initial load process

  1. gets / loads articles from a server,
  2. selects the first article from articles and
  3. uses this current article currentArticle to get the related images from the server.

THE PROBLEM

Data load from the server takes approx. 1 second to return the articles records, then as above select first article and then also return the related image records from the server. The problem is that during that latency period the second controller (ImagesController) is looking for the cached data in the second Services module (ImageDataService) and cannot find it because the first promises have obviously not resolved from the Article Controller due to server latency and as such the ArticleController hasn't cached the images data yet, which then blows up any following image related code. As you can see below, if I try to return a $q.when(cachedImages) on cachedImages, it will returns a promise, but that promise is never resolved. As both controllers are using separate $q resolve sequences it makes sense, but without building an uber controller I'm unsure how to fix the sequencing issues.

EDIT

I'm trying to solve for the n:n chaining / sequencing problem

  1. Most tutorials or discussions tend to focus on 1:1 or 1:n chaining which works perfectly. No problem there.

  2. It is the n:n where I'm having problems i.e. ctrl to service, service to service and n ctrl to service. Most of what I can find on n:n are basic tuts loading simple static arrays or object which doesn't have the latency issue.

ATTEMPTED APPROACHES

  1. rootscope / watch : I've tried $rootscope.$watch() events inside of services as well as watch in controllers i.e. an event based approach on the imageCached var inside of the ImageDataService, but frankly I find that messy as there can be unnecessary overhead, debugging and testing issues.That said, it does work but every now and then I will see lots of iteration when console logging a deeply nested var which makes the event approach seem black boxish.

EDIT - watch approach Example: I can add the following to the 2nd controller ImageController or in the ImageDataService which works, as would $emit, and then kill the watcher, but as I said this does require a bit of time management for dependent methods such as chart data directives. Also, I wondering if mixing promises and events is bad practice or is that the accepted best practice in JS?

var articleModelListener = $scope.$watch(function () {
         return ImageDataService.getImages();
     },
     function (newValue, oldValue) {
         if (newValue !== undefined && newValue !== null) {
             if (Object.keys(newValue).length > 0) {
                iCtrl.dataUrls = newValue;
                // kill $watcher
                articleModelListener();
             }

         }
     });
  1. timeout : I've also tried to wrap all ImageController code in timeout AND also document ready, but I find that has further repercussions down the line e.g. in my Chart and Poll controllers I have to wrap directives in additional $timeouts or $intervals to adjust for the ImagesController time intervals or the directives won't load the DOM attributes correctly, so it becomes a chain of app performance death.
  2. uber DataService service or factory data resolution : I've tried to resolve all data in an uber DataServices service provider but I find I have the same issue now in all controllers as although the uber service fixes the sequencing I now need to get synchronization with uber and all controllers. I know async ... give me state programming any day :)

QUESTION & ASSUMPTION

Assumption: timeout and interval wrapping are bad practices / anti-pattern as that is waterfall?

Is the best approach to stick with promises and if so is there a way to get promises to synchronize or said better sequentially resolve across multiple controllers / services OR do I keep going down the events approach using rotoscope watches and controller scope watches?

Example of my code / problem below:

PLEASE NOTE:

1. I've removed code for brevity sake i.e. I have not tested this summary code, but rather using it to example the problem above.

2. All and any help is much appreciated. Apologies for any terminology I've misused.

HTML

<section ng-controller="MainController as mCtrl"> 
    // removed HTML for brevity sakes
</section>

<section ng-controller="ImagesController as iCtrl"> 
    // removed HTML for brevity sakes
</section>

Angular JS (1.4.*)

<pre><code>
    angular.module('articles', [
        ])

        .controller('ArticlesController', ['ArticleDataServices', 'ImageDataService', function(ArticleDataServices, ImageDataService) {
            var mCtrl = this;
            mCtrl.articles = {};
            mCtrl.currentArticle = {};
            mCtrl.images = {};

            var loadArticles = function () {
                    return ArticleDataServices
                        .getArticles()
                        .then(function (articles) {
                            if(articles.data) {
                                mCtrl.articles = articles.data;
                                return mCtrl.articles[Object.keys(mCtrl.articles)[0]];
                            }
                        });
                },
                loadCurrentArticleImages = function (currentArticle) {
                    return ImageDataService
                        .getArticleImages(currentChannel)
                        .then(function (imagesOfArticles) {
                            if(imagesOfArticles.data) {
                                return mCtrl.images = imagesOfArticles.data;
                            }
                        });
                },
                cacheImages = function (images) {
                    return ImageDataService
                        .parseImages(images)
                        .then(function () {
                        });
                };

            loadChannels()
                .then(loadArticles)
                .then(loadCurrentArticleImages)
                .then(cacheImages);

        }])
    </code></pre>

NOTE : it is in the ImagesController below where things go wrong as this controller is executing its methods ahead of the first controller which is still waiting on data from server i.e. cachedImages or promise is not returning.

<pre><code>
.controller('ImagesController', ['ImageDataService', function(ImageDataService) {
    var iCtrl = this;
    mCtrl.images = {};

    var getCachedImageData = function () {
        return ImageDataService
            .getCachedImageData()
            .then(function (images) {
                if(images) {
                    return mCtrl.images = images;
                }
            });
    }
}])

.service('ArticleDataServices', ['$http',', $q', function($http, $q){
    var model = this,
        URLS = {
            ARTICLES: 'http://localhost:8888/articles'
        },
        config = {
            params: {
                'callback': 'JSON_CALLBACK',
                'method': 'GET',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}
            }
        };

    model.getArticles = function() {
        config.params['url'] = URLS.ARTICLES;
        return $http(config.params);
    };

    return {
        getArticles: function () {
            var deffered = $q.defer();
            deffered.resolve(model.getArticles());
            return deffered.promise;
        }
    }
}])

.service('ImageDataService',['$http',', $q', function($http, $q){
    var model = this,
        URLS = {
            IMAGES: 'http://localhost:8888/images'
        },
        cachedImages,
        config = {
            params: {
                'callback': 'JSON_CALLBACK',
                'method': 'GET',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}
            }
        };

    model.getArticleImages = function(currentArticle) {
        config.params['url'] = URLS.IMAGES + '/' . currentArticle.slug;
        return $http(config.params);
    };

    // Return images or $q.when 
    model.getCachedImageData = function() {
        if (cachedImages) {
            return cachedImages
        } else {
            return $q.when(cachedImages);
        }
    };

    model.setImageCache = function(images) {
        cachedImages = images;
    };

    return {
        getArticleImages: function (currentArticle) {
            var deffered = $q.defer();
            deffered.resolve(model.getArticleImages(currentArticle));
            return deffered.promise;
        },
        setImageCache:function (images) {
            return model.setImageCache(images);
        },
        getCachedImageData:function () {
            return getCachedImageData();
        }
    };
}]);

</code></pre>

1 Answer 1

1

Your problem is common for people when initializing with angular. The correct is return promise in your service as:

app.controller("AppController", function($scope, $ajax){
    $ajax.call("/people", "", "POST").then(function(req) {  
        $scope.people = req.data;
    });
});

app.factory("$ajax", function($http) {
    function ajax(url, param, method) {
        var requisicao = $http({
            method: method,
            url: url,
            data:param
        });

        var promise = requisicao.then(
            function(resposta) {
                return(resposta.data);
            }
        );
        return promise;
    }
    return({
        call:ajax
    });
});

Note that the variable is populated only in the return of service. It is important you put all methods or anything else that makes use of such a variable within Then method. This will ensure that these other methods will only be executed after returning from the backend

Sign up to request clarification or add additional context in comments.

5 Comments

Emir, thanks, however, my issue is more of a n:n issue than a chaining problem. I can use $all or chaining to resolve inside of a controller. I have no problem as that all works. The issue is the 2nd controller making a call on the expected cached data, but not finding it as the 1st controller has not resolved the promises. I could make an Uber (single large controller), but that seems to fly in the face of controller principles AND SOLID practices.
You tried use $emit after call is finished for notify the second controller?
Emir, yes $emit works. In fact and sort of broadcast - listen works. I've also added a $watch that works but I'm wondering if mixing promises and events smells of bad code practice, which is my question i.e. is there a better way?
The controllers work independently , that is, has its own life . In my view the fact that you need a direct communication enters controllers may mean that something is not right . Ideal for sharing resources between controllers is to use a service. In this service you can work with data cache as follows.
The service always returns a promise . The first controller will perform a query, and is returned after one promise , if the processing has not been completed and the second controller require the same feature also returns a promise . Thus you will have a synchronous processing both controllers will have access to information. It is important you set the variable promise within the service . In this way the controllers have only one call instance and not two

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.