AngularJS - Update model and DOM from another App - angularjs

Two angular apps are on a same page, first one have a service that modifies data model. Calling the service from its parent app modifies data and DOM accordingly.
But calling same service from another app makes a copy of data and makes changes on that copy only.
How could angular service be called from another app so it have access to data model from it's parent app?
Here's a plunk
<div ng-app="app1">
<div ng-controller="myctrl">
<input type="text" value="{{data.list.name}}">
<br>
<button ng-click="update()">run from app1</button>
</div>
</div>
<div id='app2div' ng-app='app2' ng-controller="ctrl2">
<button ng-click="update2()">run from app2</button>
</div>
<script>
var test = angular.module("app1",[]);
test.controller("myctrl", function($scope, service)
{
$scope.scope1 = true;
$scope.data=service.data;
$scope.update=function()
{
service.updateValue();
};
}).factory("service", function()
{
return new (function(){
this.data={list:{name:"name0"}},
this.updateValue=function()
{
var self=this;
self.data.list=self.values[self.count];
self.count++;
},
this.values= [{name:"name1"}, {name:"name2"}, {name:"name3"}],
this.count=0
})();
});
var app2 = angular.module('app2',['app1']);
app2.controller('ctrl2', function($scope, service){
$scope.scope2 = true;
$scope.update2 = function()
{
service.updateValue();
};
});
angular.bootstrap(document.getElementById("app2div"), ['app2']);
</script>

Explanation
There is not really any built in support for cross-app communication in AngularJS, so it can be a bit tricky. But it is possible.
First it's important to understand the difference between module, app and instances.
In this case we will use the word app to describe the entire composition of modules.
In your case you have:
app1 - which consists of the module app1.
app2 - which consists of the modules app2 and app1.
Now when you use either ng-app or angular.bootstrap to bootstrap an application a new instance of the app is created.
Each instance gets its own instance of the $injector service.
Now comes an important part - each service in AngularJS is a singleton in the sense that it is only created once per $injector instance (including $rootScope).
This means that after you have bootstrapped the both applications you will have:
Instance of app1 -> Instance of $injector -> Instance of service
Instance of app2 -> Instance of $injector -> Instance of service
So there are two instances of service, each with their own internal state. Which is why the data is not shared in your case.
So it wouldn't even matter if you instead bootstrapped app1 two times, there would still be two instances of everything.
How to
To retrieve an app´s injector instance you can use the injector method that is available on angular elements.
Note that this is not the same as the method angular.injector, which is used to create entire new instances of $injector.
Retrieve a reference to a DOM element of a specific app that was used with ng-app or angular.bootstrap:
var domElement = document.getElementById('app1');
Turn it into an angular (jqLite) element:
var angularElement = angular.element(domElement);
Retrieve the app's injector:
var injector = angularElement.injector();
Use the injector to retrieve the correct instance of a service:
var myService = injector.get('myService')
Call the service:
myService.doSomething();
If this is done outside of Angular's digest loop you will need to trigger it manually:
var rootScope = injector.get('$rootScope');
rootScope.$apply();
Or:
var scope = angularElement.scope();
scope.$apply();
Example solution
We have two apps:
var app1 = angular.module('app1', ['shared']);
var app2 = angular.module('app2', ['shared']);
app1.controller('MyController', function($scope, sharedFactory) {
$scope.sharedFactory = sharedFactory;
});
app2.controller('MyController', function($scope, sharedFactory) {
$scope.sharedFactory = sharedFactory;
})
Both apps use the shared module consisting of a factory named sharedFactory with the following api:
var api = {
value: 0,
increment: increment,
incrementSync: incrementSync
};
Remember that each app has its own instance of the factory.
The increment method will simply increment the value. If called from app1 the incrementation will take place in app1's instance of the service.
The incrementSync method will increment the value in both app1's and app2's service instance:
function incrementSync() {
var app1InjectorInstance = angular.element(document.getElementById('app1')).injector();
var app2InjectorInstance = angular.element(document.getElementById('app2')).injector();
console.log(app1InjectorInstance === app2InjectorInstance); // false
var app1SharedFactoryInstance = app1InjectorInstance.get('sharedFactory');
var app2SharedFactoryInstance = app2InjectorInstance.get('sharedFactory');
console.log('app1-sharedFactory:', app1SharedFactoryInstance);
console.log('app2-sharedFactory:', app2SharedFactoryInstance);
app1SharedFactoryInstance.increment();
app2SharedFactoryInstance.increment();
var otherInjectorInstance = app1SharedFactoryInstance === api
? app2InjectorInstance
: app1InjectorInstance;
otherInjectorInstance.get('$rootScope').$apply();
}
Note that if incrementSync is for example called from within app1, we will be in app1's digest loop, but we will have to start app2´s digest loop manually. This is what the part involving otherInjectorInstance does.
Demo: http://plnkr.co/edit/q2ZEG8VqxHORRzLy8wqZ?p=preview
Hopefully this will help you get going and find a solution to your problem.

Related

Best way to update a variable in another controller in AngularJs

I have a controller (call it "A") where I get a value from the webserver. When I get this value, I store it in a Service.
In another controller (call it "B") I have to get this value from the service everytime it is stored in the service. And this value must appear in the view (updated).
My usual solution is:
I emit an event everytime I store the value in the service. then in the controller B I listen to this event and then i get the value from the service.
I know there are other solutions, like the scope.$watch/apply but I don't know which is better.
Can you suggest me which way is better?
Push Values from a Service with RxJS
One alterantive to $rootScope.broadcast is to build a service with RxJS Extensions for Angular:
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);
app.factory("DataService", function(rx) {
var subject = new rx.Subject();
var data = "Initial";
return {
set: function set(d){
data = d;
subject.onNext(d);
},
get: function get() {
return data;
},
subscribe: function (o) {
return subject.subscribe(o);
}
};
});
Then simply subscribe to the changes.
app.controller('displayCtrl', function(DataService) {
var $ctrl = this;
$ctrl.data = DataService.get();
var subscription = DataService.subscribe(function onNext(d) {
$ctrl.data = d;
});
this.$onDestroy = function() {
subscription.dispose();
};
});
Clients can subscribe to changes with DataService.subscribe and producers can push changes with DataService.set.
The DEMO on PLNKR.
Watchers are called everytime a $digest or an $apply cycle is done. It has more impact on your application than a local event like you are doing.
If you can use services to control communication between directives and/or controllers, it's better.
As far as i know, there's 4 ways to handle communication between controller and/or directives:
Using a service (like you do)
Rely on the $apply cycle with $watch
Use the angular event system (with scope.$emit or scope.$broadcast)
Be very dirty and use a global variable
Using a service is the best way. Especially if you handle a "one-to-one" communication.

Why can't I inject $scope into a factory in Angular?

I have a factory that needs to listen for a broadcast event. I injected $scope into the factory so I could use $scope.$on. But as soon as I add $scope to the parameter list I get an injector error.
This works fine:
angular.module('MyWebApp.services')
.factory('ValidationMatrixFactory', ['$rootScope', function($rootScope) {
var ValidationMatrixFactory = {};
return ValidationMatrixFactory;
}]);
This throws an injector error:
angular.module('MyWebApp.services')
.factory('ValidationMatrixFactory', ['$scope', '$rootScope', function($scope, $rootScope) {
var ValidationMatrixFactory = {};
return ValidationMatrixFactory;
}]);
Why can't I inject $scope into a factory? And if I can't, do I have any way of listening for events other than using $rootScope?
Because $scope is used for connecting controllers to view, factories are not really meant to use $scope.
How ever you can broadcast to rootScope.
$rootScope.$on()
Even though you can't use $scope in services, you can use the service as a 'store'. I use the following approach inspired on AltJS / Redux while developing apps on ReactJS.
I have a Controller with a scope which the view is bound to. That controller has a $scope.state variable that gets its value from a Service which has this.state = {}. The service is the only component "allowed" (by you, the developer, this a rule we should follow ourselves) to touch the 'state'.
An example could make this point a bit more clear
(function () {
'use strict';
angular.module('app', ['app.accounts']);
// my module...
// it can be defined in a separate file like `app.accounts.module.js`
angular.module('app.accounts', []);
angular.module('app.accounts')
.service('AccountsSrv', [function () {
var self = this;
self.state = {
user: false
};
self.getAccountInfo = function(){
var userData = {name: 'John'}; // here you can get the user data from an endpoint
self.state.user = userData; // update the state once you got the data
};
}]);
// my controller, bound to the state of the service
// it can be defined in a separate file like `app.accounts.controller.js`
angular.module('app.accounts')
.controller('AccountsCtrl', ['$scope', 'AccountsSrv', function ($scope, AccountsSrv) {
$scope.state = AccountsSrv.state;
$scope.getAccountInfo = function(){
// ... do some logic here
// ... and then call the service which will
AccountsSrv.getAccountInfo();
}
}]);
})();
<script src="https://code.angularjs.org/1.3.15/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="AccountsCtrl">
Username: {{state.user.name ? state.user.name : 'user info not available yet. Click below...'}}<br/><br/>
Get account info
</div>
</div>
The benefit of this approach is you don't have to set $watch or $on on multiple places, or tediously call $scope.$apply(function(){ /* update state here */ }) every time you need to update the controller's state. Also, you can have multiple controllers talk to services, since the relationship between components and services is one controller can talk to one or many services, the decision is yours. This approach focus on keeping a single source of truth.
I've used this approach on large scale apps... it has worked like a charm.
I hope it helps clarify a bit about where to keep the state and how to update it.

How to change parent controller's object instance using an AngularJS service?

I'm using nested controllers and UI-Router. My top level controller, called MainCtrl, is set in my app's index.html file. If the MainCtrl uses a service, to pass data around, how can I change an instance of an object in the MainCtrl from a child controller without using $scope?
This is basically what I have (typed from memory):
var mainCtrl = function (ProfileSvc) {
var vm = this;
vm.profile = ProfileSvc.profile;
};
var loginCtrl = function (ProfileSvc, AuthSvc) {
var vm = this;
vm.doLogin = function (form) {
if (form.$error) { return; }
AuthSvc.login(form.user, form.pass).
.then(function(response) {
ProfileSvc.profile = response.data.profile;
}, function(errResponse) {
// error
}
};
};
User #shershen posted a reply to another question that gave me the idea to use $scope.$on and an event, however I really do not want references to $scope in my code:
Propagating model changes to a Parent Controller in Angular
I think without using $scope you may want to use the Controller as ctrl in your views. So...
var mainCtrl = function (ProfileSvc) {
var vm = this;
vm.profile = ProfileSvc.profile;
vm.updateProfile = function(profileAttrs) {
vm.profile = ProfileSvc.update(profileAttrs);
}
};
Then in the view, something along the lines of:
<div ng-controller="mainCtrl as main">
<button ng-click="main.updateProfile({ name: 'Fishz' })">
</div>
Hope this helps!
I had to do something similar on a project and ended up using $cacheFactory. First just load it up as a service with something like:
myApp.factory('appCache', function($cacheFactory) {
return $cacheFactory('appCache');
});
Then make sure you inject appCache into your controllers and then in your controllers you can call the cache service's put and get methods to store and retrieve your object.
In my case the parent view and child view both can change the object I'm caching, but the user only can commit from the parent.

AngularJS - service changes controller data

I discovered that when I call a service method within my controller and pass to it an object as a parameter, any changes that are done to that object (inside service method) are also made to the original object from my controller.
I always thought that controller data should stay unchanged until I changed it inside promise win/error event and only if I need to.
JS sample:
// Code goes here
var app = angular.module('App', []);
app.controller('AppCtrl', function($scope, simpleService){
$scope.data = { d: 1, c: 10};
$scope.clickMe = function(){
simpleService.clickMe($scope.data).then(function(res){
alert($scope.data.d);
})
.catch(function(err){
alert($scope.data.d);
});
}
});
app.factory('simpleService', function($q){
var simpleServiceMethods = {};
simpleServiceMethods.clickMe = function(data){
var deffered = $q.defer();
//data = JSON.parse(JSON.stringify(data)); - solution: clone data without references
data.d = 1111;
deffered.reject();
return deffered.promise;
}
return simpleServiceMethods;
});
Plunker demo: http://plnkr.co/edit/nHz2T7D2mJ0zXWjZZKP3?p=preview
I believe this is the nature of angular's databinding. If you want to pass the details of a $scope variable you could make use of angular's cloning capability with copy or update your services to work slightly differently by creating a copy on the service side. Normal CRUD style applications you'd normally be passing the id of an entity, receiving a new entity or posting changes which may in most cases already be present client side.

angular: write a service which mocks my real backend

I need to write code which accesses data in a backend.
Trouble is that backend is not ready yet. So I want to inject a service which would, possibly by configuration, use the real backend or a mock object.
I think angular services are what I need. But I am not clear how to implement this.
app.factory('myService', function() {
var mySrv;
//if backend is live, get the data from there with $http
//else get data from a mock json object
return mySrv;
});
Somehow I guess I'd have to write two more services, the real one and the fake one, and then call the one or the other in 'myService'?
Maybe I totally misunderstand mocking, but I'd rather not want this to be mocked for test runs (not for unit tests like in this post: Injecting a mock into an AngularJS service - I'd like the app to really use my mock for demo and development purposes.
This is actually where Angular's dependency injection system really comes in handy. Everything in Angular is basically a provider at some level. Methods like service and factory are just convenience functions to avoid boilerplate code.
A provider can be configured during the bootstrap process, which is really handy for setting up scenarios exactly like what you are describing.
At it's simplest, a provider just needs to be a constructor function that creates an object with a $get function. The $get is what creates the actual services, and this is where you can check to see which one to create.
//A simple provider
function DataServiceProvider(){
var _this = this;
_this.$get = function(){
//Use configured value to decide which
// service to return
return _this.useMock ?
new MockService() :
new RealService;
};
}
Now you can register this as a provider with your application module.
angular.module('my-module', [])
.provider('dataService', DataServiceProvider);
And the provider can be configured before it creates the first service instance. By convention, the provider will be available as NAME + 'Provider'
angular.module('my-module')
.config(['dataServiceProvider', function(dataServiceProvider){
//Set the flag to use mock service
dataServiceProvider.useMock = true;
}]);
Now whenever you inject dataService anywhere in your application, it will be using the mock service you provided based on configuration.
You can see a full working example of this in the snippet below.
(function(){
function RealService(){
this.description = 'I\'m the real McCoy!';
}
function MockService(){
this.description = 'I\'m a shady imposter :P';
}
function DataServiceProvider(){
var $this = this;
$this.useMock = false;
$this.$get = function(){
return $this.useMock ?
new MockService() :
new RealService();
};
}
function CommonController(dataService){
this.dataServiceDescription = dataService.description;
}
CommonController.$inject = ['dataService'];
angular.module('common', [])
.provider('dataService', DataServiceProvider)
.controller('commonCtrl', CommonController);
angular.module('provider-app-real-service', ['common'])
.config(['dataServiceProvider', function(dataServiceProvider){
dataServiceProvider.useMock = false;
}]);
angular.module('provider-app-mock-service', ['common'])
.config(['dataServiceProvider', function(dataServiceProvider){
dataServiceProvider.useMock = true;
}]);
var moduleNames = ['provider-app-real-service','provider-app-mock-service'];
angular.forEach(moduleNames, function(modName){
//Have to manually bootstrap because Angular only does one by default
angular.bootstrap(document.getElementById(modName),[modName]);
});
}());
<script src="http://code.angularjs.org/1.3.0/angular.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet" />
<div class="container">
<div class="row" id="provider-app-real-service">
<div class="col-sm-12" ng-controller="commonCtrl as ctrl">
<h1>{{ctrl.dataServiceDescription}}</h1>
</div>
</div>
<div class="row" id="provider-app-mock-service">
<div class="col-sm-12" ng-controller="commonCtrl as ctrl">
<h1>{{ctrl.dataServiceDescription}}</h1>
</div>
</div>
</div>
Does this need to be a service? If you want to create a mock service then Josh's answer above is perfect.
If your not tied to using a service then I suggest looking at my answer to the following question Mock backend for Angular / Gulp app which also about mocking out a backend. Regardless of wether your backend is created or not mocking it out allows for more stable test runs and development of you app.

Resources