admin管理员组

文章数量:1417720

I have a single-page AngularJS application posed 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 ponent
  • 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 ponent

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 ponent'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);
    });
}

I have a single-page AngularJS application posed 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 ponent
  • 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 ponent

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 ponent'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);
    });
}
Share Improve this question edited Jan 18, 2016 at 18:58 Maxime Rossini asked Jan 18, 2016 at 15:33 Maxime RossiniMaxime Rossini 3,9094 gold badges36 silver badges51 bronze badges 6
  • Does the service have knowledge of which controllers will be subscribing to its services? Or does it matter? What does the service need to know from its clients in order to function? – georgeawg Commented Jan 18, 2016 at 17:39
  • You could look at how ui-riouter implements their resolve method github./angular-ui/ui-router/blob/master/src/resolve.js – Daniel Lizik Commented Jan 18, 2016 at 17:51
  • The service has no knowledge of its subscribers, it just calls a notify() inner method when an event occurs, which will in turn call each registered callback with some arguments (really this is just a custom implementation of $on and $broadcast to avoid polluting the global event names). The service does not need to know anything about its subscribers to function, but it needs to be initialized with a set of parameters (here an authentication token provided by the app.initializer module). – Maxime Rossini Commented Jan 18, 2016 at 17:53
  • @Daniel_L It seems to be a lot of code for such a trivial need :( Does angular provide no reliable way at all to run some code after everything else has been initialized? Like the run() method, but executing after the controllers? Is it my design which is totally biased, or is no one actually using things like OAuth2 token parsing (to initialize a 3rd party ponent) in the angular munity? – Maxime Rossini Commented Jan 18, 2016 at 18:01
  • @MaximeRossini If you want the code in your controller to execute after something has run (i.e., after a promise is resolved) you'd need a way to either (a) return the constructor instance inside a callback (not possible), or (b) do some plicated low-level modifications to angular's internal $controller service to get your instantiation in a promise chain. It's a "trivial need" but trying to fight async is never easy. – Daniel Lizik Commented Jan 18, 2016 at 18:28
 |  Show 1 more ment

3 Answers 3

Reset to default 2

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 remended 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.

Store the status in the service

function padService() {
  var ctx = this;
  ctx.status = 'authenticating';
  return {
      initialize: function (authToken) { ... },
      onAuthenticated: function (callback) { ... },
      onReady: function (callback) { ... },
      onError: function (callback) { ... },
      getStatus: function() { return ctx.status; }
  };
}

In your directive get the status from the service instead of defining it.

function widgetController($scope, padService) {
  $scope.message = padService.getStatus();
  padService.onAuthenticated(function () {
    $scope.message = "authenticated";
  });
  padService.onReady(function () {
    $scope.message = "ready";
  });
  padService.onError(function () {
    $scope.message = "error";
  });
}

There's are a lot of room for improvements here but for a start, the code above allows sharing of the same data throughout the module from the service.

Next thing you might want to do is just have one subscriber method to broadcast changes made to the status to the listeners

Adding on for a more plete solution

Service

padService.$inject = ['$rootScope'];
function padService($rootScope) {
  return {
    status: "authenticating",
    initialize: function (authToken) { 
      //Update the status
      $rootScope.$broadcast('STATUS_CHANGED');
    },
    subscribe: function(scope, callback){
      var ctx = this;
      scope.$on('STATUS_CHANGED', function (){
        callback(ctx.status);
      });
    }
  };
}

Controller

function widgetController($scope, padService) {
  $scope.status = padService.status;
  padService.subscribe($scope, function(status){
    $scope.status = status;
  });
}

本文标签: javascriptIn AngularJShow to run code after all controllers are initializedStack Overflow