Trying to make my Angular application work in live mode and prototype mode by overriding services. When the prototype mode is activated in the config, the bootstrap process is paused, mock service files are loaded, and then bootstrapping resumes.
Below is a simplified version of the source code for the demo:
App.js
The main app that calls a service for simulation and displays the result. It requires the StubApp
for service overrides.
var app = angular.module('app', ['StubsApp'])
.run([ '$rootScope', 'DataService', function($scope, DataService){
DataService.getData().then(function(data){
$scope.name = data;
});
}]);
DataService.js
A simple service registered with the app.
function DataService($q){
this.getData = function(){
return $q.when('I am Real!!');
}
}
DataService.$inject = ['$q'];
angular.module('app').service('DataService',DataService);
Driver.js
Config registration file that sets up mocking.
angular.module('app').config(['$provide', 'stubServiceProvider', 'AppConfig', function($provide, stubProvider, AppConfig){
if(AppConfig.StubEnabled){
stubProvider.loadStubsInModule('plunker');
}
}]);
StubProvider.js
An interface similar to angular.module
for registering stub services. It loads mock services from stubs.json
by halting the bootstrap, allowing an App to override existing services with the ones in stubs.json
.
var Stubs = {},
modules = [];
function module(moduleName) {
return {
mock: function (func) {
modules.push(func);
}, get: function () {
return modules;
}
};
}
Stubs.module = module;
loadStubs();
function loadStubs() {
window.name = "NG_DEFER_BOOTSTRAP!";
var injector = angular.injector(['ng']);
var $q = injector.get('$q');
var $http = injector.get('$http');
var scripts = [];
$http.get('stubs.json').then(function (result) {
scripts = result.data.map(function (src) {
var script = document.createElement('script');
script.src = src;
script.async = true;
document.head.appendChild(script);
var deferred = $q.defer();
script.onload = function () {
deferred.resolve();
};
return deferred.promise;
});
$q.all(scripts).finally(function () {
angular.element().ready(function () {
angular.resumeBootstrap();
});
});
});
}
//The provider that handles the service overriding
angular.module('StubsApp', []).provider('stubService', function ($provide) {
...... //Code in plunker
});
DataService Mock.js
A mock service using the Stubs interface to register the mock
. It includes a property Stubs.module('app').mock(MockService)
stubFor="serviceName"
which specifies the service it mocks.
function MockService($q, $log){
this.getData = function(){
return $q.when('I am Mock!!');
}
}
MockService.$inject = ['$q', '$log'];
MockService.stubFor="DataService";
Stubs.module('app').mock(MockService);
stubs.json
A JSON file listing mock services.
["DataServiceMock.js"]
index.html
<script src="app.js"></script>
<script src="DataService.js"></script>
<script src="Driver.js"></script>
<script src="stubprovider.js"></script>
When Driver.js
is moved above DataService.js
, the mocking stops working. The specific code causing the issue in "StubProvider.js" is:
Stubs.module(moduleName).get().forEach(function (mod) {
var serviceName = mod.stubFor;
var ctor = mod;
if (serviceName) {
$provide.service(serviceName, ctor);
}
});
Check out the Demo Plnkr. Commenting out the line in Driver.js
will show real service output instead of mock service. To replicate the issue, move Driver.js
before DataService.js
in the index.html file, preventing the override of DataService with MockDataservice.
Why does the order of config registration matter when the config phase should run before service instantiation?
Is there a better way to ensure all scripts are loaded before resuming the bootstrap process without using the deferred pattern?