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:
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);
});
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With