Share data, events across multiple angular modules - javascript

I'm developing some project where I use multiple modules
I have a kernel module and other modules that depend on kernel
angular.module('kernel', []);
...
angular.('process-manager', [ 'kernel', 'ui.router' ])
...
etc
I need to share some data across all modules and also broadcast some events across all modules.
For now in child modules I'm using $rootScope of kernel module that defined as a global in the $window object
.factory('$appScope', appScope)
...
appScope.$inject = ['$window', '$rootScope'];
function appScope($window, $rootScope: ng.IRootScopeService) {
if (angular.isDefined($window.superScope) === false) {
$window.superScope = $rootScope;
}
return $window.superScope;
}
Is there any better solution to do things like this?
EDIT
kernel module bootstraped through ng-app and other modules bootstraped through angular.bootstrap();

I see you're using .factory()... in your code, and even though I'm not sure exactly how you use it, you might be on the right path.
Use services. Services are singletons that are meant to hold data that can be shared through injection across your app and even across modules. As for broadcasting events, you can call $broadcast on your $rootScope, or you can $watch changes on your services' data in all modules.
There's a better answer to a similar question here.
Here's an example:
angular.module('firstModule', []).service('MyService', function () {
return {
some: 'data'
};
}).controller('someController', function ($scope, MyService) {
$scope.$watch(function () {
return MyService.some;
}, function (some) {
// This runs whenever the service's data changes
console.log(some.data, 'logged in first module');
}, true);
});
angular.module('secondModule', ['firstModule']) // <-- this makes the first module's service available for injection in the second module
.controller('someController', function ($scope, MyService) {
$scope.$watch(function () {
return MyService.some;
}, function (some) {
// This runs whenever the service's data changes
console.log(some.data, 'logged in second module');
}, true);
})
Cum grano salis
The service may be instantiated anew for every module, which may impede your ability to communicate across modules. Please fact-check me on this. In the meantime, local storage as suggested in Dennis Nerush's answer may be the better way to go. I have found https://github.com/gsklee/ngStorage to be a good complement for usage with Angular.

I suggest to use local storage to pass data between modules and keep the data consistent . Here is a great example
http://www.codediesel.com/javascript/sharing-messages-and-data-across-windows-using-localstorage/

Register an event listner in the required modules as '$scope.$on('changedVariable', function (event, args){//do your thing in listning module});'
And in the module from where you need to braodast:
$scope.$broadcast('changedVariable', { change: someValue});
And you are done, precisely! ;)

Related

How to change Angular factory to resolve before injection

I have an existing application that uses a MapProvider like so:
mapModule.factory('MapProvider', ['$injector', function($injector) {
return $injector.get('GoogleMapsService');
}]);
This MapProvider is used extensively across the application and is injected into various other controllers and services (rightly or wrongly).
I now need to add a BaiduMapsService, which I have been able to get working as a test with:
mapModule.factory('MapProvider', ['$injector', function($injector) {
if(true) {
return $injector.get('GoogleMapsService');
} else {
return $injector.get('BaiduMapsService');
}
}]);
And flipping the if value accordingly. (Both of these services are using a TypeScript interface, so have the same methods). Now, I need to add a $http call to the API, which will return which map to use, based on the provided data. How can I make my factory asynchronous, without having to change all my MapProvider.someCallHere() calls to MapProvider.then(m => m.someCallHere()).
Ideally, when MapProvider is injected across my application, it will be able to resolve using the async data (only once), and then inject the necessary service afterwards.
Alternatively, is there a way to defer / delay loading Angular at all, until I make an API call and set some global data somewhere?
Thanks.
You can postpone the application bootstrap (also, don't use ng-app, do it manually) until you get data from server. I've answered this before on this question but each case has its own specific details.
I usually see a config value being declared on the app before the application gets bootstraped, this is very useful for multi-tenant apps. So that this preference values can be used in the whole app as an injected provider.
For example:
var app = angular.module('app', []);
// retrieve the $http provider
var ngInjector = angular.injector(["ng"]);
var $http = ngInjector.get("$http");
// load config function. Returns a promise.
function loadConfig(){
return $http.get("/config.json").then(function(response) {
// declare the configuration value on your app
app.constant("Config", response.data);
}, function(err) {
console.error("Error loading the application config.", err)
});
}
// Call loadConfig then bootstrap the app
loadConfig().then(function () {
angular.element(document).ready(function() {
angular.bootstrap(document, ["app"]);
});
});
Finally from your factory, you can use the Config constant to retrieve the preferred map.
mapModule.factory('MapProvider', ['$injector', 'Config', function($injector, Config) {
if(Config.preferedMap == 'GoogleMap') {
return $injector.get('GoogleMapsService');
} else {
return $injector.get('BaiduMapsService');
}
}]);
Only way I can think is to hold initialize whole angular (and modules) until you got your "config" (and set is as global variable).

How to test the config function of an Angular module?

I'm defining some setup code in the config function of an Angular module that I want to unit test. It is unclear to me how I should do this. Below is a simplified testcase that shows how I'm getting stuck:
'use strict';
angular.module('myModule', []).config(['$http', '$log', function($http, $log) {
$http.get('/api/getkey').then(function success(response) {
$log.log(response.data);
});
}]);
describe('myModule', function() {
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
angular.module('myModule');
$httpBackend.flush();
}));
});
This is clearly not the right way because I get the following error:
Error: No pending request to flush !
A complete, ready-to-run Angular project with the above testing code can be found on GitHub. If you know what to do with this scenario, please answer here on Stack Overflow. Bonus points if you also submit a pull request to the GitHub repo.
Use run instead of config if your initialization requires services to be injected. The config function can only receive providers and constants as parameters, not instantiated services like $http (relevant docs).
angular.module('myModule', []).run(['$http', '$log', function($http, $log) {
...
}]);
Initialize your module for testing
beforeEach(module('myModule'));
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
$httpBackend.flush();
}));
So the full working version looks like
'use strict';
angular.module('myModule', []).run(['$http', '$log', function($http, $log) {
$http.get('/api/getkey').then(function success(response) {
$log.log(response.data);
});
}]);
describe('myModule', function() {
beforeEach(module('myModule'));
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
$httpBackend.flush();
}));
});
Also, here's an example of testing the config block to check that a method on a provider was called: https://medium.com/#a_eife/testing-config-and-run-blocks-in-angularjs-1809bd52977e#71e0
mzulch is right to point out that services cannot be injected in an angular.module(...).config block. He also provides the right solution for the scenario where you actually need to use services in module initialization code: use the .run block instead of the .config block. His answer works perfectly for this scenario.
The question of how to write a unit test for the .config block remains. Let's adapt the naieve code from my question to a scenario where .config is actually warranted. The following snippet injects a provider dependency instead of a service dependency:
angular.module('myModule', []).config(['$httpProvider', function($httpProvider) {
$httpProvider.useApplyAsync(true);
}]);
describe('myModule', function() {
it('configures the $http service to combine response processing via $applyAsync', inject(function($httpProvider) {
angular.module('myModule');
expect($httpProvider.useApplyAsync()).toBeTruthy();
}));
});
This time, the implementation of 'myModule' is correct. The unit test however, which is analogous to the attempt in my question, is still incorrect. Now Karma gives me the following error:
Error: [$injector:unpr] Unknown provider: $httpProviderProvider <- $httpProvider
This cryptical error is coming from the inject which is passed as the second argument to the it. Note how Provider is being stuttered. This is caused by the fact that inject is looking for the provider for $httpProvider. A "meta provider", as we may call it. Such things don't exist in the Angular framework, but inject is trying it anyway because it expects you to only ask for service dependencies. Services do have providers, for example, $http has $httpProvider.
So inject (full name: angular.mock.inject, here available globally) is not the right way to get hold of $httpProvider in the testcase. The right way is to define an anonymous module configuration function using module (angular.mock.module) which closes over a variable in which we can capture the provider. This works because providers can be injected at configuration time (see the link at the bottom of mzulch's answer as well as my own answer to my other question for details on configuration time vs run time). It looks like this:
var $httpProvider;
beforeEach(function() {
module(function(_$httpProvider_) {
// this is a .config function
$httpProvider = _$httpProvider_;
});
// after this I can use inject() to make the magic happen
});
Another mistake in my naieve testcase is that I'm trying to execute 'myModule's configuration steps by calling angular.module('myModule'). For testcase purposes, I should be using the global module (angular.mock.module) instead, and the wisest place to do so is in the beforeEach fixture. In conclusion, the following code does the job:
describe('myModule', function() {
var $httpProvider;
beforeEach(function() {
module(function(_$httpProvider_) {
$httpProvider = _$httpProvider_;
});
module('myModule');
});
it('configures the $http service to combine response processing via $applyAsync', function() {
inject(); // enforces all the module config steps
expect($httpProvider.useApplyAsync()).toBeTruthy();
});
});
I opted to put the inject() at the start of my testcase, but I could also put it at the end of the beforeEach. The advantage of the latter approach would be that I can write the call to inject in one place and not need to repeat it in every testcase. The advantage of the approach actually taken here is that more modules can be added to the injector in later beforeEaches or even in individual testcases.
I pushed this alternative solution to a new branch on GitHub.

AngularJS trigger and watch object value change in service from controller from another module (Extended)

Referring to this question
AngularJS trigger and watch object value change in service from controller
It is trying to watch for changes in a service from a controller.
Im trying to extend it to handle multiple module (concurrent app in same page's different div's) communication.
Problem:
I want to achieve the similar feat, but with slight different scenario. I have two modules myApp and yourApp for example. myApp has a service. I want to watch changes in myApp's service within yourApp module's controller. Can it be done? or is there different method to reflect and detect data changes of one module inside another module.
Consider the example code below:
HTML
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div ng-click="setFTag()">Click Me</div>
</div>
</div>
<div ng-app="yourApp">
<div ng-controller="yourCtrl">
</div>
</div>
Javascript
// myApp module
var myApp = angular.module('myApp',[]);
// myApp's service
myApp.service('myService', function() {
this.tags = {
a: true,
b: true
};
this.setFalseTag = function() {
alert("Within myService->setFalseTag");
this.tags.a = false;
this.tags.b = false;
//how do I get the watch in YourCtrl of yourApp module to be triggered?
};
});
// myApp's controller
myApp.controller('MyCtrl', function($scope, myService) {
//$scope.myService = myService;
$scope.setFTag = function() {
alert("Within MyCtrl->setFTag");
myService.setFalseTag();
};
/*
$scope.$watch(function () {
return myService.tags;
}, function(newVal, oldVal) {
alert("Inside watch");
console.log(newVal);
console.log(oldVal);
}, true);
*/
});
// yourApp module (Injecting myApp module into yourApp module)
var yourApp = angular.module('yourApp',['myApp']);
// yourApp's controller
yourApp.controller('YourCtrl', function($scope, myService) {
$scope.$watch(function () {
return myService.tags;
}, function(newVal, oldVal) {
alert("Inside watch of yourcontroller");
console.log(newVal);
console.log(oldVal);
}, true);
});
Note: I am not sure if this is the right way to communicate between modules. Any suggestion or the solution will be highly appreciated.
P.S. Ive bootstrapped two modules to fit into same page.
JSFiddle
Communicating between modules is a whole different story from communicating between apps.
In the case of communicating between modules, it's only a matter of injecting module A into module B.
In which case, module B has complete access to inject/communicate with anything exposed in module A.
Communicating between apps however, is somewhat more complicated. You would need to setup something 'outside' the angular world, accepting properties from both applications.
I've put together a jsBin, showcasing how you can do it.
With that said, I'm not sure I would recommend it - but then again best practices on the subject do not exist afaik.
The gist of it is;
Setup a new instance of a shared service that lives outside the Angular world.
Attach the instance of said service to window.
Access the above instance in your app specific services/controllers through $window.
Register each $scope that needs to access the data stored in the shared service.
Upon updating data in the shared service, loop through the registered $scopes and trigger an $evalAsync(so as to not end up with $digest already in progress).
Watch as your data is synced across applications.
This is just a PoC on how to do it, I would not recommend it as it sort of blows when it comes to unit testing. And also because we are exposing properties on window. yuck.
Sure, you could disregard from exposing on window - but then your code would have to live in the same file (afaic). Yet another, yuck.
To build upon this, if you were to decide (or, be able to) only use a single app (multiple modules is just fine) and want to communicate/sync a service with multiple components I reckon this would be the best way to do so:
app.service('shared', function () {
var data = {
a: true,
b: true
};
this.toggle = function() {
data.a = !data.a;
data.b = !data.b;
};
Object.defineProperty(this, 'data', {
get: function () {
return data;
}
});
});
By using an object getter, you won't need to setup a $watch to sync data across your components within the same module. Nor trigger manual $digest's.
another jsbin - showcasing the above.
The difference between two modules and two apps as I see it:
2 modules
Two separate modules that gets 'bundled' together into a single application (either by a third module, or injecting module A into B).
var app1 = angular.module('app1', ['app2', 'app3']);
var app2 = angular.module('app2', []);
var app3 = angular.module('app3', []);
angular.bootstrap(/*domElement*/, app1);
2 Apps
var app1 = angular.module('app1', []);
var app2 = angular.module('app2', []);
angular.bootstrap(/*domElement1*/, app1);
angular.bootstrap(/*domElement2*/, app2);
I don't think there is any point in having two applications and share state between the two. I think the whole point with running two applications is to separate the two. Otherwise it's just over engineering imho.
Some thoughts:
You could add a common dependency to both applications. It probably wont be a shared state, but you would have access to the same implementation in both apps.
You could possibly utilise sessionStorage as a medium of transportation for your data between the two applications. Just make sure to cleanup afterwards :)

Is it good practise to use angular.element("[ng-controller="someCtrl"]").scope()

Is it good practise to use angular.element("ng-controller="someCtrl"]").scope() instead of using factory to handle data flow between controllers using dependency injection. The problem here is I want to call a function of another controller, so there are two ways either I put it in a factory and reuse it among controllers or use above syntax to call the function directly.
If you need to call a function from other controller, it should be a SERVICE/Factory. This way, you will share code between controllers, and you will code with good practices.
As they say in angularjs docs
Angular services are substitutable objects that are wired together
using dependency injection (DI). You can use services to organize and
share code across your app.
Then, you just need to create a service or a factory
//The service
angular.module('myApp')
.service('serviceName', function ($http, $scope, socket) {
//This functions will be available in your controller
return {
list: function () {
$http.get(listUrl).success(function (lista) {
$scope.centerList = lista;
socket.syncUpdates('center', $scope.centerList);
});
}
};
});
//The controller
angular.module('myApp').controller('myCtrl', function ($scope, centerService) {
$scope.listCenters = function () {
centerService.list();
};
});
Just to clarify, and to add some comprehensive ideas about services and factories:
https://www.youtube.com/watch?v=J6qr6Wx3VPs
AngularJS: Service vs provider vs factory
https://www.airpair.com/angularjs/posts/top-10-mistakes-angularjs-developers-make
It is never good practice to access the DOM from a controller. So if wrapping the method in a factory/service is an option, I'd say that's the way to go.

Setting Modules in AngularJS app

I'm building an AngulrJS module that I intend to share, for free, with other developers via GitHub/bower. While my approach currently works, I'm concerned about name collisions. The way that I am currently setting up my module is like this:
var myModuleName = angular.module('myModuleName', []);
myModuleName.factory('$myFirstServiceName', function() {
...
});
myModuleName.factory('$mySecondServiceName', ['$q', function($q) {
...
}]);
myModuleName.factory('$myThirdServiceName', ['$q', function($q) {
...
}]);
My concern is that global 'myModuleName'. While this approach works, the name of my module isn't that "special". For that reason, I'm concerned it will collide with a developers existing stuff or cause other problems down the road.
Is there any way to more elegantly create a module where I don't have to worry about naming collisions?
Thank you!
You can use prefix in module name, which present project/library/author.
Like pasvaz.bindonce, ui.router etc...

Categories

Resources