Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In AngularJS, how to run code after all controllers are initialized?

I have a single-page AngularJS application composed of multiple modules, whose purpose is to provide the user with a collaborative pad (main widget) and other related widgets (other connected users, pad metadatas, etc.).

I chose to split the application as follow:

  • 1 module hosting a service, responsible for exposing initialization methods for the pad component
  • N modules hosting custom directives (and their controller), corresponding to the different widgets in the application
  • 1 module responsible for gathering parameters and initializing the pad component

Let's simplify this by assuming I have only 1 widget, whose sole goal is to display a status message to the user: "authenticating", "authenticated", "error" or "ready".

I chose to use a subscribe/notify pattern with the service to let the widget be notified of a change in the shared component's state.

The service:

angular.module("app.core").factory("padService", padService);
function padService() {
    // Callback registration and notification code omitted
    return {
        initialize: function (authToken) { ... },
        onAuthenticated: function (callback) { ... },
        onReady: function (callback) { ... },
        onError: function (callback) { ... }
    };
}

The widget:

angular.module("app.widget").directive("widget", widget);
function widget() {
    return {
        templateUrl: 'app/widget.html',
        restrict: 'E',
        controller: widgetController
    };
}
function widgetController($scope, padService) {
    $scope.message = "authenticating";
    padService.onAuthenticated(function (user) {
        $scope.message = "authenticated";
        // Do other stuff related to user authentication event
    });
    padService.onReady(function (padInstance) {
        $scope.message = "ready";
        // Do other stuff related to pad readiness event
    });
    padService.onError(function (error) {
        $scope.message = "error";
        // Do other stuff related to error event
    });
}

Now the "initializer module", in its simplest form, gathers an authentication token authToken from the URL fragment (similar to OAuth2) and simply calls padService.initialize(authToken);. Note that it could as well be a dedicated authentication popup, that's why it resides in its own module.

My problem is that I don't know where to put that piece of code. All the places I tried resulted in being placed too early in the angular bootstrap process and/or not updating the widget:

angular.module("app.initializer").run(run);
function run($document, $timeout, tokenService, padService) {
    // This does not work because run() is called before the
    // controllers are initialized (widget does not get notified)
    var authToken = tokenService.getTokenFromUrl();
    padService.initialize(authToken);

    $document.ready(function () {
        // This does not work because angular does not detect
        // changes made to the widget controller's $scope
        var authToken = tokenService.getTokenFromUrl();
        padService.initialize(authToken);

        // This does not work in firefox for some reason (but
        // does in chrome!)... except if I enter debug mode or
        // set the timeout to a longer value, which makes it
        // either really difficult to diagnostic or ugly as hell
        $timeout(function () {
            var authToken = tokenService.getTokenFromUrl();
            padService.initialize(authToken);
        }, 0);
    });
}
like image 873
Maxime Rossini Avatar asked Oct 30 '22 11:10

Maxime Rossini


1 Answers

The controllers are created synchronously (I assume), so there shouldn't be any difficulty to make some code run after that.

That is an erroneous assumption. The AngularJS framework routinely creates and destroys directives and their controllers during the life of the application. ng-repeat, ng-if, ng-include, etc. all create and destroy DOM containing directives. If your "widget" is part of an ng-repeat, its controller gets instantiated multiple times, once for each item in the list that ng-repeat watches.

To retain data that persists throughout the lifetime of an application, keep it in a service. (Or on $rootScope; not recommended but an option.) Controllers can't assume that they have been started during bootstrap. They need to "catch-up" and subscribe to changes.

Keep persistent data in a factory service and provide setter and getter functions.

angular.module("app").factory("padService", function () {
    //Store service status here
    var status = "none-yet";

    function setStatus(s) {
        status = s;
        return status;
    };

    function getStatus() {
        return status;
    };

    return {
        setStatus: setStatus,
        getStatus: getStatus
    };
});

In your "widget", inject the service, subscribe to changes, and "catch-up".

angular.module("app").directive("widget", function() {
    function widgetController($scope, padService) {
        //subscribe with $watch
        $scope.$watch(padService.getStatus, function(newStatus) {
            //catch-up and react to changes
            case (newStatus) {  
                "authenticated":
                     // Do stuff related to authenticated state
                     break;
                "ready":
                     // Do stuff related to pad ready state
                     break;
                "error":
                     // Do stuff related to error state
                     break;
                default:
                     // Do something else
             }
            $scope.message = newStatus;
        };
    };
    return {
            templateUrl: 'app/widget.html',
            restrict: 'E',
            controller: widgetController
    }
});

When the directive first registers the listener using $watch, the AngularJS framework, executes the watch function (in this case padService.getStatus), and executes the listener function. This allows the directive to "catch up" to the current status of the service.

On each digest cycle, the AngularJS framework executes padService.getStatus. If the status has changed, the framework executes the listener function with the new status as the first parameter. This allows the directive to react to changes.

You can not assume that the directive and its controller are created synchronously. But you do know that the service is instantiated and its constructor function executed before it is injected into the controller.

like image 163
georgeawg Avatar answered Nov 12 '22 17:11

georgeawg